Ask pattern
tell is fire-and-forget — it returns immediately and you never see
the actor’s response. Most actor-to-actor communication is fine like
that. But sometimes you need a reply: an HTTP handler that asks
an actor for state and serializes it as a response, a saga that needs
the result of step N before issuing step N+1, a test that wants to
assert on a return value.
ask is the helper for those cases. It sends a message to an actor,
attaches a temporary “reply-to” ref under the hood, and returns a
Promise that resolves with the first reply or rejects on timeout.
A minimal example
Section titled “A minimal example”import { Actor, ActorSystem, Props, type ActorRef } from 'actor-ts';
type Cmd = | { readonly kind: 'inc' } | { readonly kind: 'get'; readonly replyTo: ActorRef<number> };
class Counter extends Actor<Cmd> { private count = 0; override onReceive(cmd: Cmd): void { if (cmd.kind === 'inc') this.count++; else if (cmd.kind === 'get') cmd.replyTo.tell(this.count); }}
const system = ActorSystem.create('demo');const counter = system.spawnAnonymous(Props.create(() => new Counter()));
counter.tell({ kind: 'inc' });counter.tell({ kind: 'inc' });
// Method-on-ref form — terse, infers the reply type:const current = await counter.ask<number>({ kind: 'get' }, 5_000);console.log(current); // 2
await system.terminate();Two equivalent shapes:
// Method on every ActorRef — infers Reply from the type argument:ref.ask<Reply>(message, timeoutMs?): Promise<Reply>
// Free function — useful in places where you want the explicit form:function target: ActorRef<TReq>.ask<TRes = unknown>(message: OmitReplyTo<TReq>, timeoutMs: number = 5_000,): Promise<TRes>The OmitReplyTo<TReq> parameter type subtracts replyTo from
every variant of the message union that declares it — callers never
write the field themselves. The framework allocates a one-shot
reply ref, attaches it both as context.sender and (when the
message shape expects it) as message.replyTo, and resolves the
promise with the first reply.
The replyTo field convention
Section titled “The replyTo field convention”ask works by sending your message with a temporary reply-to ref
attached. The receiver replies by tellling that ref.
By convention, the framework uses replyTo as the field name in
the request message, and the recipient pattern-matches on it the
same as any other field. Look at the Cmd type above: the 'get'
variant has replyTo: ActorRef<number>.
For the recipient, it’s just a regular tell on a regular ref:
if (cmd.kind === 'get') { cmd.replyTo.tell(this.count);}Inside the recipient, you can also reach the same ref via
this.sender — it’s an Option<ActorRef>, and ask attaches the
temporary ref through that channel too:
if (cmd.kind === 'get') { this.sender.forEach((replyTo) => replyTo.tell(this.count));}Either works. Using an explicit replyTo field makes the type
contract more obvious (the request shape declares it expects a
reply of type ActorRef<number>); using this.sender is more
implicit but works for actors that handle both ask-style and
fire-and-forget messages.
Timeouts + the AskTimeoutError
Section titled “Timeouts + the AskTimeoutError”import { AskTimeoutError } from 'actor-ts';
try { const result = await counter.ask({ kind: 'get' }, 1_000); console.log(result);} catch (e) { if (e instanceof AskTimeoutError) { console.log('counter did not reply in time'); } else { throw e; }}The timeout is mandatory in spirit — defaulting to 5 seconds catches the case where you forgot to think about it. Tune per call site:
- Cheap in-process replies: 100-500 ms.
- Cross-cluster lookups: 1-5 s depending on network expectations.
- Anything that might hit a slow downstream (DB query, HTTP call inside the actor): match the underlying-call’s timeout + a small buffer.
When the timeout fires, the temporary reply-to ref is stopped. A
late reply from the actor goes to dead letters and the
AskTimeoutError rejects the Promise. The actor itself isn’t
affected — it has no idea the asker gave up.
Replying with an Error
Section titled “Replying with an Error”A recipient can reject an ask by replying with an Error
instance. The ask Promise rejects with that error rather than
resolving with the message.
class Service extends Actor<Cmd> { override onReceive(cmd: Cmd): void { if (cmd.kind === 'fetch') { if (this.unavailable) { cmd.replyTo.tell(new Error('service unavailable')); return; } cmd.replyTo.tell(this.data); } }}
try { const data = await service.ask({ kind: 'fetch' });} catch (e) { // e is the actor's "service unavailable" Error.}This is a deliberate convention — the framework recognizes
Error-typed replies and rejects the Promise. Carry domain errors
the normal way (typed via the reply union) for everything else;
reserve the Error-reply path for genuine failure states.
When NOT to use ask
Section titled “When NOT to use ask”ask is convenient but it has overhead — it allocates a temporary
ref, registers it with the system, and tears it down on
resolution/timeout. Three situations where plain tell is the
right call instead:
Where to next
Section titled “Where to next”- Messages — the
replyTo: ActorRef<T>field convention covered in detail. - Actor —
this.senderand how the framework wires it. - ActorSystem — where temporary ask refs are registered and torn down.
- CircuitBreaker — when the ask’s downstream call is flakey and you want to fail fast rather than time out.
- Future patterns —
pipeTo,after, sequence — composing async results back into the actor world withoutawait-blocking.
The ask function API reference
documents the full signature and error types.