Перейти к содержимому
Русский

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 { match } from 'ts-pattern';
import { Actor, ActorSystem, type ActorRef } from 'actor-ts';
type IncrementCommand = { readonly kind: 'increment' };
type GetCommand = { readonly kind: 'get'; readonly replyTo: ActorRef<number> };
type Command = IncrementCommand | GetCommand;
class Counter extends Actor<Command> {
private count = 0;
override onReceive(command: Command): void {
match(command)
.with({ kind: 'increment' }, () => this.onIncrement())
.with({ kind: 'get' }, (c) => this.onGet(c))
.exhaustive();
}
private onIncrement(): void { this.count++; }
private onGet(command: GetCommand): void { command.replyTo.tell(this.count); }
}
const system = ActorSystem.create('demo');
const counter = system.spawnAnonymous(Counter);
counter.tell({ kind: 'increment' });
counter.tell({ kind: 'increment' });
// 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();

ask is a method on every ActorRef:

// Method on every ActorRef — infers Reply from the type argument:
ref.ask<Reply>(message, timeoutMs?): Promise<Reply>

The OmitReplyTo<TMessage> 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 Command type above: the 'get' variant has replyTo: ActorRef<number>.

For the recipient, it’s just a regular tell on a regular ref:

private onGet(command: GetCommand): void {
command.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:

private onGet(command: GetCommand): void {
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 AskTimeoutError rejects the Promise. The temporary reply-to ref is a one-shot: once it has settled — on the timeout or on the first reply — any later reply is silently discarded in place (it does not go to dead letters). 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<Command> {
override onReceive(command: Command): void {
match(command)
.with({ kind: 'fetch' }, (c) => this.onFetch(c))
.exhaustive();
}
private onFetch(command: FetchCommand): void {
if (this.unavailable) {
command.replyTo.tell(new Error('service unavailable'));
return;
}
command.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 ActorRef class API reference documents the ask method’s full signature and error types.