Become and stash
An actor’s onReceive is normally a single function that handles
every message kind. Two context APIs let it reshape that
handling on the fly:
context.become(handler)— temporarily replace the current receive handler with a different one. Restore withunbecome().context.stash()+unstashAll()— park messages an actor isn’t ready for, then re-deliver them when it is.
Together they’re the actor-model answer to “this actor has phases where it doesn’t accept normal traffic” — initialization, recovery from a checkpoint, draining before shutdown, awaiting an async config load.
become — swap the behavior
Section titled “become — swap the behavior”import { Actor, ActorSystem, Props } from 'actor-ts';
type Msg = | { readonly kind: 'on' } | { readonly kind: 'off' } | { readonly kind: 'press' };
class Switch extends Actor<Msg> { override onReceive(msg: Msg): void { this.offState(msg); }
private offState = (msg: Msg): void => { if (msg.kind === 'press') { this.log.info('turning on'); this.context.become(this.onState); } };
private onState = (msg: Msg): void => { if (msg.kind === 'press') { this.log.info('turning off'); this.context.become(this.offState); } };}
const system = ActorSystem.create('demo');const sw = system.actorOf(Props.create(() => new Switch()));
sw.tell({ kind: 'press' }); // → "turning on"sw.tell({ kind: 'press' }); // → "turning off"sw.tell({ kind: 'press' }); // → "turning on"become(handler) swaps the function the framework calls on the
next message. No flag fields, no inline if (this.state === 'on')
ladder — the behavior is the state.
The signature:
become(behavior: Receive<TMsg>, discardOld?: boolean): void;unbecome(): void;discardOld defaults to true (replace). Pass false to push
the new behavior onto a stack instead — unbecome() then pops it
and restores the previous one.
Stacked behaviors
Section titled “Stacked behaviors”class Loader extends Actor<Cmd> { override onReceive(msg: Cmd): void { this.idle(msg); }
private idle = (msg: Cmd): void => { if (msg.kind === 'load') { this.context.become(this.loading, /* discardOld */ false); this.startLoading(); } };
private loading = (msg: Cmd): void => { if (msg.kind === 'done') { this.context.unbecome(); // pops loading, idle returns } };}Stacking is the right tool when you have a transient sub-behavior
(“currently loading”, “currently authenticating”) that returns to a
stable base behavior once it’s done. For toggles like the Switch
example above, discardOld = true (the default) is cleaner.
Compared to a plain state field
Section titled “Compared to a plain state field”Without become, the same Switch would look like this:
class Switch extends Actor<Msg> { private isOn = false; override onReceive(msg: Msg): void { if (msg.kind === 'press') { this.isOn = !this.isOn; this.log.info(this.isOn ? 'turning on' : 'turning off'); } }}Two states — the plain-field version is shorter. Where become
wins is at N states: with three or four phases, the inline
if/else chain on isOn grows into a state-machine ladder that
the type system can’t help you with. become makes each phase its
own function, with its own valid-message set, naturally narrowed.
For a more formalized version of the same idea, see the FSM pattern — explicit state + transition declarations on top of the same engine.
stash — park, then replay
Section titled “stash — park, then replay”import { Actor, ActorSystem, Props } from 'actor-ts';
type Msg = | { readonly kind: 'configure'; readonly url: string } | { readonly kind: 'request'; readonly payload: string };
class Worker extends Actor<Msg> { private url?: string;
override onReceive(msg: Msg): void { if (msg.kind === 'configure') { this.url = msg.url; // Now drain whatever piled up while we were unconfigured. this.context.unstashAll(); } else if (msg.kind === 'request') { if (!this.url) { // Not ready yet — park this message. this.context.stash(); return; } this.log.info(`POST ${this.url}: ${msg.payload}`); } }}The flow: messages arriving before configure are stashed. Once
configure runs, unstashAll() re-prepends them onto the mailbox
in the order they came in, and the actor processes them with the
URL now set.
The signatures:
stash(): void;unstashAll(): void;readonly stashSize: number;Three details that matter:
stash()must be called from inside a user-message handler. It parks the currently-handled message. Calling it frompreStart, a timer callback that isn’t an actor message, or outsideonReceivethrowsStashOutsideHandlerError.unstashAll()is FIFO. Messages come back in the order they were stashed. For aPriorityMailbox, they’re re-inserted through the priority function — so an unstashed message rejoins its priority tier, not the absolute front of the queue.- The stash has a capacity. By default the buffer is bounded
to prevent memory leaks from runaway stashing; overflow throws
StashOverflowError. Configure via system settings if you need larger / smaller; see Configuration.
stash + become — the canonical combo
Section titled “stash + become — the canonical combo”The two APIs compose for the “actor with an init phase” pattern:
class Worker extends Actor<Msg> { private url?: string;
override preStart(): void { // Switch immediately into the "loading config" behavior. this.context.become(this.loading); void this.loadConfig(); }
private loading = (msg: Msg): void => { if (msg.kind === 'configure') { this.url = msg.url; this.context.become(this.ready); this.context.unstashAll(); } else { // Anything else arriving during load — stash it. this.context.stash(); } };
private ready = (msg: Msg): void => { if (msg.kind === 'request') { this.log.info(`POST ${this.url}: ${msg.payload}`); } };
override onReceive(msg: Msg): void { // Initial behavior — the preStart switches into 'loading' before // any user message arrives. This is here to satisfy the signature. this.loading(msg); }
private async loadConfig(): Promise<void> { const cfg = await fetchConfig(); this.context.self.tell({ kind: 'configure', url: cfg.url }); }}Two behaviors: loading (stashes everything except configure),
ready (handles requests). The become makes the transition
explicit; the stash + unstashAll makes sure no traffic is
dropped during the initialization window.
When to reach for which
Section titled “When to reach for which”| Situation | Tool |
|---|---|
| Two or more discrete phases with different valid-message sets | become |
| One phase has a transient sub-phase that should restore on done | become(..., discardOld=false) + unbecome() |
| Boolean flag — “ready”/“not ready” — with the same handler shape | A plain field (don’t reach for become) |
| Some messages must be deferred until later, in order | stash + unstashAll |
| Init phase that buffers all non-init traffic | become(loading) + stash in loading + unstashAll in transition |
Where to next
Section titled “Where to next”- Actor — the base class
whose
onReceiveyou swap withbecome. - Mailboxes —
unstashAllre-inserts into the mailbox; the mailbox decides the order. - FSM — a more formal
state-machine style on top of the same
becomeengine. - Typed behaviors — the typed
API expresses the same idea as functional
Behavior<T>values rather than mutating context.
The ActorContext API
reference covers become, stash, and the full surface.