Behaviors
The Behaviors namespace is a collection of combinators that
build Behavior<T> values. A behavior describes what the actor
does when the next message arrives — and what behavior it adopts
after that. An actor’s lifetime is just a sequence of behaviors:
each handler returns the next one.
import { Behaviors, spawnTyped, ActorSystem } from 'actor-ts';
type Msg = { kind: 'tick' };
const ticker = Behaviors.receive<Msg>((ctx, msg) => { ctx.log.info(`tick at ${Date.now()}`); return Behaviors.same; // keep being a ticker});
const system = ActorSystem.create('demo');const ref = spawnTyped(system, ticker);Behaviors.receive builds the most common behavior: a handler that
runs on every message and returns the next behavior. The same
sentinel says “stay as I am” — the same closure handles the next
message.
The four constructors
Section titled “The four constructors”receive — handler with context + message
Section titled “receive — handler with context + message”const counter = (n: number): Behavior<Msg> => Behaviors.receive<Msg>((ctx, msg) => { switch (msg.kind) { case 'inc': return counter(n + 1); case 'dec': return counter(n - 1); case 'get': msg.replyTo.tell(n); return Behaviors.same; }});The handler receives both a TypedActorContext<T> (for spawning
children, logging, watching) and the message. Returns the next
behavior — Behaviors.same keeps the closure; a fresh
counter(n + 1) adopts a new closure with updated state.
receiveMessage — handler without context
Section titled “receiveMessage — handler without context”const counter = (n: number): Behavior<Msg> => Behaviors.receiveMessage<Msg>((msg) => { if (msg.kind === 'inc') return counter(n + 1); return Behaviors.same;});Shortcut for the common case where you don’t need the context.
Equivalent to Behaviors.receive((_ctx, msg) => ...).
receiveWithSignal — handler + lifecycle signals
Section titled “receiveWithSignal — handler + lifecycle signals”const watcher = Behaviors.receiveWithSignal<Msg>( (ctx, msg) => { // handle user message... return Behaviors.same; }, (ctx, signal) => { if (signal.kind === 'terminated') { ctx.log.info(`watched actor ${signal.ref.path} stopped`); } return Behaviors.same; },);The signal handler fires for lifecycle events:
| Signal kind | When |
|---|---|
'post-stop' | The actor is stopping. Use for cleanup. |
'pre-restart' | The supervisor is about to restart the actor. signal.reason is the error. |
'terminated' | A watched actor stopped. signal.ref is its ref. |
This is the typed-DSL equivalent of overriding postStop,
preRestart, and handling Terminated messages in the untyped
form.
setup — capture the context once
Section titled “setup — capture the context once”const myActor = Behaviors.setup<Msg>((ctx) => { ctx.log.info(`I'm starting at ${ctx.path}`); const helper = ctx.spawn(helperBehavior, 'helper'); return Behaviors.receive((_ctx, msg) => { helper.tell(msg); // helper captured in closure return Behaviors.same; });});setup runs once when the actor starts. Use it for one-time
initialization that the receive-handler should close over:
spawning children, capturing ctx.self for the children to know,
opening external connections.
The decorators
Section titled “The decorators”Three combinators wrap another behavior with extra capabilities:
withTimers
Section titled “withTimers”import { Behaviors, type TimerScheduler } from 'actor-ts';
const heartbeat = Behaviors.withTimers<Msg>((timers) => { timers.startTimerWithFixedDelay('hb', { kind: 'tick' }, 5_000);
return Behaviors.receiveMessage((msg) => { if (msg.kind === 'tick') console.log('heartbeat'); return Behaviors.same; });});The TimerScheduler API is the same one context.timers gives
in the untyped form. withTimers captures it in a closure so
the receive handler has access without going through ctx.timers
on every message.
withStash
Section titled “withStash”const init = Behaviors.withStash<Msg>(100, (stash) => { return Behaviors.receive((ctx, msg) => { if (msg.kind === 'ready') { stash.unstashAll(); // replay all buffered messages return ready; } stash.stash(msg); // park everything else for later return Behaviors.same; });});
const ready = Behaviors.receive<Msg>((ctx, msg) => { // handle messages normally return Behaviors.same;});Capacity-bounded stash, with stash / unstashAll / isEmpty /
isFull / size. Same semantics as
the untyped context.stash,
exposed as a value rather than via context.
supervise(behavior).onFailure(strategy)
Section titled “supervise(behavior).onFailure(strategy)”import { Behaviors, OneForOneStrategy, Directive } from 'actor-ts';
const supervised = Behaviors .supervise(myReceiveBehavior) .onFailure(new OneForOneStrategy( (err) => Directive.Restart, { maxRetries: 5, withinTimeRangeMs: 60_000 }, ));Wrap a behavior with a supervisor strategy. Errors thrown from the inner handler are routed through the strategy — Restart re-initializes the behavior (resets to its initial form), Stop terminates, Resume skips the failing message.
See Supervision for the directive semantics; they apply identically in the typed form.
The five sentinels
Section titled “The five sentinels”Values you return from a handler to express a transition decision:
| Sentinel | Meaning |
|---|---|
Behaviors.same | Keep the current behavior. The handler closure runs again on the next message. |
Behaviors.stopped | Stop the actor. Equivalent to context.stopSelf() in the untyped form. |
Behaviors.unhandled | This message isn’t handled here; route to dead letters. |
Behaviors.empty | The behavior accepts messages but does nothing. Useful as a placeholder. |
Behaviors.ignore | Drop every message silently (no dead-letter routing). |
The first three are the most useful day-to-day. empty and
ignore exist for special cases — a “this actor is intentionally
silent for now” stub or a sink that should swallow traffic.
A multi-behavior example
Section titled “A multi-behavior example”import { Behaviors, type Behavior, spawnTyped, ActorSystem } from 'actor-ts';
type Msg = | { kind: 'configure'; url: string } | { kind: 'request'; payload: string };
const initializing = Behaviors.withStash<Msg>(100, (stash) => Behaviors.receive<Msg>((ctx, msg) => { if (msg.kind === 'configure') { const url = msg.url; stash.unstashAll(); return ready(url); } stash.stash(msg); return Behaviors.same; }),);
const ready = (url: string): Behavior<Msg> => Behaviors.receive<Msg>((ctx, msg) => { if (msg.kind === 'request') { ctx.log.info(`POST ${url}: ${msg.payload}`); } return Behaviors.same; });
const system = ActorSystem.create('demo');spawnTyped(system, initializing);Two behaviors:
initializingstashes everything until aconfigurearrives, then transitions toready(url)after replaying the stash.readyhandles requests using the capturedurl.
The transitions are explicit returns; the state lives in closure
parameters; there’s no this to worry about.
Composition order
Section titled “Composition order”Decorators compose outside-in. Behaviors.supervise(Behaviors.withTimers(...))
means “supervise the timers-using behavior”; Behaviors.withTimers(Behaviors.supervise(...))
means “give the supervised inner the timers.” In practice:
const supervised = Behaviors .supervise(Behaviors.withTimers((timers) => Behaviors.receive((ctx, msg) => Behaviors.same) )) .onFailure(strategy);supervise is outside the withTimers, so the strategy
oversees the whole construction. This is almost always the right
nesting.
Where to next
Section titled “Where to next”- Typed actor — the runtime that interprets a Behavior.
- Spawn typed —
spawnTyped,typedProps,spawnTypedChild. - Supervision — what
Behaviors.supervise(...).onFailure(...)uses internally. - Become and stash (untyped) —
the OO equivalents of
Behaviors.withStash+ behavior-switching via return values.