Skip to content

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 with unbecome().
  • 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.

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.

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.

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.

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 from preStart, a timer callback that isn’t an actor message, or outside onReceive throws StashOutsideHandlerError.
  • unstashAll() is FIFO. Messages come back in the order they were stashed. For a PriorityMailbox, 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.

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.

SituationTool
Two or more discrete phases with different valid-message setsbecome
One phase has a transient sub-phase that should restore on donebecome(..., discardOld=false) + unbecome()
Boolean flag — “ready”/“not ready” — with the same handler shapeA plain field (don’t reach for become)
Some messages must be deferred until later, in orderstash + unstashAll
Init phase that buffers all non-init trafficbecome(loading) + stash in loading + unstashAll in transition
  • Actor — the base class whose onReceive you swap with become.
  • MailboxesunstashAll re-inserts into the mailbox; the mailbox decides the order.
  • FSM — a more formal state-machine style on top of the same become engine.
  • 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.