Location-transparent messaging
In a cluster you message an actor by name, and the runtime routes to whichever node owns it. The PID you get back is a routed handle: tell, ask, request, watch, unWatch, forward, and pipeTo on it keep their call sites and their semantics, and the address decides whether the message stays local or crosses the wire. Replying to ctx.sender routes back to the sender's node, wherever that is.
An actor's path already carries the address of the node it lives on (nodeakt://orders@10.0.0.5:4000/name), and the distributed registry maps each name to its owning node. Looking a name up joins the two: read the owner from the registry, get a handle that routes there.
actorOf and actorOfAsync
There are two lookups, and the difference is whether they wait for the network.
const local = system.actorOf("orders"); // PID | undefined, never blocks
const anywhere = await system.actorOfAsync("orders"); // Promise<PID | undefined>, resolves cross-nodeactorOf(name) | actorOfAsync(name) | |
|---|---|---|
| Returns | PID | undefined | Promise<PID | undefined> |
| Blocks on the network | Never | Only on a cold miss (one registry read) |
| Local actor | Returns it | Returns it |
| Cross-node actor already in this node's view | Returns a routed handle | Returns a routed handle |
| Cross-node actor not yet in this node's view | Returns undefined | Reads the registry, returns a routed handle |
| Use it for | The hot path, local lookups, code that must not await | Reaching a name placed anywhere, the usual application lookup |
actorOf answers instantly from a warm view, the routing cache this node builds from the placements it makes, from resolution reads, and from relocation events. That makes it perfect for the hot path, but it means the first lookup of a name owned by another node, one this node has not learned yet, reads as undefined even though a live owner exists. actorOfAsync closes that gap by awaiting a single registry read when the view is cold, so an application that reaches an actor placed on some other node gets it on the first call.
Rule of thumb: reach for actorOfAsync in application code that looks a name up to message it. Keep actorOf for lookups that must stay synchronous and are local or already warm.
What a routed handle does
A routed handle carries the remoting contract:
tellreturnsnullwhen the transport accepted the message, not when the far mailbox did. An undeliverable message, an unknown name, a full mailbox, a stopped target, becomes a dead letter on the node that discovered it.ask/requestcarry the reply back and settle on it; a timeout rejects withErrRequestTimeout, sentinel errors keep their identity across the wire.- Ordering holds per actor: messages to one actor ride a single lane keyed by its path, so per-actor order survives the hop.
watchdelivers oneTerminatedwhen the actor stops or its node dies; node death and connection loss are deliberately indistinguishable.isRunning()isfalsefor a routed handle; liveness across nodes is not synchronously knowable. Watch it instead.
Following a moved actor
A routed handle pins nothing. When an actor relocates to another node, the routing cache heals by re-resolving: the next time the resolver reads a cached owner that membership no longer lists, it drops the entry and reads the registry again, so lookups reach the actor's new home. A send that raced the move can be lost, an ask to the old owner is refused and re-resolves, a tell dead-letters, which is the message-loss window relocation accepts.
Watch is per-incarnation. A relocation stops the old instance and starts a fresh one, so a watcher receives Terminated even though the name lives on elsewhere. To keep following a relocatable name, re-look it up with actorOfAsync and re-watch; a RelocationCompleted subscriber can automate exactly that.