Skip to content
English

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.

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.

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.

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.

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.

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:

  • Messages — the replyTo: ActorRef<T> field convention covered in detail.
  • Actorthis.sender and 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 patternspipeTo, after, sequence — composing async results back into the actor world without await-blocking.

The ask function API reference documents the full signature and error types.