Actor Isolation and Message Passing
Actors keep mutable state private and exchange typed values across checked boundaries.
An actor owns its state and handles one message at a time, including across suspension. Code outside the actor cannot reach that state directly. It holds a typed actor handle and calls a receive fn instead.
This separation matters because actors may run concurrently. Keeping mutable state behind one handler seat removes the shared-memory data race that would occur if two actors could update the same collection at once.
Send an ordinary value
type Parcel {
label: string;
}
actor Inbox {
var received: i64 = 0;
receive fn accept(parcel: Parcel) -> i64 {
received += 1;
println(parcel.label);
received
}
}
fn main() {
let inbox = spawn Inbox;
let parcel = Parcel { label: "books" };
let count = inbox.accept(parcel).expect("inbox available");
println(f"accepted: {count}");
println(parcel.label); // ordinary data remains usable by the sender
stop(inbox);
stopped(inbox);
}The actor call crosses a boundary. Inbox receives an independent logical snapshot of parcel, while main keeps its own value. The runtime may copy, retain immutable storage, or use copy-on-write storage as long as later mutation stays isolated.
The call itself waits for the handler to finish and returns a Result<i64, ActorError>. Here, expect says that an unavailable actor should stop the caller. Production code can match or propagate the error instead.
Understand Send
Send is a marker with no methods. It means a value can safely cross an actor boundary. The compiler derives it from the whole type: a record is sendable when all its fields are sendable; the same rule applies through enum payloads, tuples, options, results, and collections. Users do not manually implement Send for a type.
Send answers “is this crossing safe?” It does not mean “deep-copy these bytes.” Scalars, immutable-shareable values, ordinary collections, actor handles, and admitted single-owner resources can all be sendable while using different transfer mechanisms.
State stays with its actor
Sending an actor handle names the same destination; it does not copy the actor's state. Sending ordinary data gives the receiver a snapshot. Values such as Rc<T> and borrowed views stay within their owning actor and are rejected at the boundary.
Transfer an affine value
A sendable affine value has one owner, so passing it in a local actor call transfers that owner into the request. The sender cannot use it again. If delivery is explicitly rejected before admission, Hew's policy API can return the still-owned request for a safe retry; other failures do not imply that retry is safe.
#[resource]
type Permit {
id: i64;
}
impl Permit {
fn close(consume self) {
println(f"released permit {self.id}");
}
}
actor Worker {
receive fn run(permit: Permit) {
println(f"using permit {permit.id}");
} // the actor-side owner releases the permit
}
fn main() {
let worker = spawn Worker;
let permit = Permit { id: 3 };
worker.run(permit).expect("worker available");
stop(worker);
stopped(worker);
}The transfer preserves one cleanup responsibility. A remote message has an additional requirement: it must be serializable data. Process-local handles and resources do not become network values simply because a local transfer is admitted.
Choose completion or submission
| You need | Write | What returns |
|---|---|---|
| Handler completion | actor.method(args) | A typed Result after the turn finishes |
| Concurrent completion | let task = fork actor.method(args) | A task; await task yields the same completion result |
| Submission without waiting for the turn | mailbox(actor).method(args) | A delivery outcome after admission; only unit-returning handlers are allowed |
Mailbox capacity is unbounded unless the actor declares a bound. A bounded actor can wait, reject, drop, or coalesce submissions according to its declaration. Completion calls are not silently discarded or superseded. See the actor guide's backpressure section for the policies.
Advanced: two lifetimes meet at a call
The caller owns the request operation and its reply. Once the destination claims a request, the actor owns the handler turn. If the caller's deadline expires afterward, the caller can abandon the reply, but that does not undo the actor's effects or cancel its independent lifetime. This is why a timeout is not proof that a request did nothing.
An actor call returning Err is an ordinary value for the caller to handle. A fault in the actor belongs to the actor's lifecycle and supervisor. The completion envelope keeps those concerns visible rather than presenting every call as an infallible function call.
Continue from here
- Build a two-actor service in the tutorial.
- Read the actor guide for handlers, lambda actors, streams, supervision, and backpressure.
- Look up exact send and handler contracts in the released actor reference.
- Follow actor construction and cleanup in Actor Lifecycle and Supervision.