Skip to main content

Ask pattern

The ask pattern bridges the actor world — asynchronous and message-driven — with code that needs a response. Call ask() on an ActorRef, get a Future, and block on await() when you need the result.

Request-reply sequence

Figure 1: The ask pattern. ask() creates a FutureSlot and a single-use FutureRef. The target actor replies via ctx->reply(), which routes through the FutureRef to resolve the slot.

How it works

  1. ask() creates a FutureSlot (a runtime-specific suspension primitive) and a lightweight FutureRef — a single-use ActorRef whose tell() resolves the slot.
  2. The message is enqueued to the target's mailbox with the FutureRef carried as the envelope's sender reference.
  3. The target processes the message and calls $ctx->reply($response), which routes the response back through the FutureRef.
  4. Future::await() suspends the current fiber or coroutine until the reply arrives or the timeout fires.
  5. If the timeout expires before a reply, AskTimeoutException is thrown.

Signature

src/Actor/ActorRef.php
/**
* @template R of object
* @param T $message
* @return Future<R>
* @throws AskTimeoutException
*/
#[NoDiscard]
public function ask(object $message, Duration $timeout): Future;

The #[NoDiscard] attribute causes static analysis to warn if you discard the return value.

Usage

src/Http/AccountController.php
use Monadial\Nexus\Runtime\Duration;

final readonly class GetBalance {}

final readonly class Balance
{
public function __construct(public float $amount) {}
}

$future = $accountRef->ask(new GetBalance(), Duration::seconds(5));
$balance = $future->await();

echo $balance->amount;

The target actor replies via $ctx->reply(). No $replyTo field is needed on the message — routing is automatic through the envelope's sender reference.

src/Actor/AccountActor.php
use Monadial\Nexus\Core\Actor\Behavior;
use Monadial\Nexus\Core\Actor\ActorContext;

$behavior = Behavior::receive(
static function (ActorContext $ctx, object $msg): Behavior {
if ($msg instanceof GetBalance) {
$ctx->reply(new Balance(amount: 42.50));
}

return Behavior::same();
},
);

Future combinators

Future supports map() and flatMap() for transforming results without blocking until await().

src/Service/CurrencyService.php
$future = $accountRef
->ask(new GetBalance(), Duration::seconds(5))
->flatMap(static function (object $balance) use ($exchangeRef): Future {
assert($balance instanceof Balance);

return $exchangeRef->ask(
new ConvertCurrency($balance->amount, 'USD'),
Duration::seconds(5),
);
});

$converted = $future->await();

Both map() and flatMap() are lazy — the transformation runs only when await() is called.

Timeout handling

src/Service/AccountService.php
use Monadial\Nexus\Core\Exception\AskTimeoutException;
use Monadial\Nexus\Runtime\Duration;

try {
$balance = $accountRef
->ask(new GetBalance(), Duration::seconds(3))
->await();
} catch (AskTimeoutException $e) {
$logger->warning("Ask to {$e->target} timed out after {$e->timeout}");
}

AskTimeoutException exposes the target actor path and the elapsed Duration.

When to use ask vs tell

tell()ask()
DirectionFire-and-forgetRequest-response
BlockingNoYes — blocks the calling fiber on await()
ReturnvoidFuture<R>
ThroughputHigherLower — allocates FutureSlot, suspends fiber
CouplingLooseTighter — sender depends on timely response
Error modelHandled by supervisionSurfaces as AskTimeoutException

Prefer tell() for actor-to-actor communication. Reserve ask() for:

  • Edge of the actor system — when non-actor code (HTTP controllers, CLI commands) needs a result.
  • Orchestration — when a coordinator must gather replies from several children before proceeding.
  • Testing — to assert that an actor produces the expected reply.

Runtime support

RuntimeFutureSlotSuspension mechanism
FiberRuntimeFiberFutureSlotFiber::suspend() / resume on next tick
SwooleRuntimeSwooleFutureSlotSwoole\Coroutine\Channel(1)
StepRuntimeStepFutureSlotFiber::suspend() (deterministic, test-controlled)

Failure modes

Ask failures fall into three categories: timeouts, dead targets, and missing reply paths.

SymptomCauseRecovery
AskTimeoutException thrownThe target actor did not call $ctx->reply() within the timeoutVerify the handler covers the message type and calls reply(); increase Duration only after diagnosing the actual latency
AskTimeoutException thrown immediatelyask() was sent to DeadLetterRef or a stopped actorCheck $ref->isAlive() before calling ask(); handle the exception at the call site
Future resolves with the wrong typeThe handler called $ctx->reply() with an unexpected message typeAlign the reply type with the Future<R> type parameter; use Psalm generics to catch this at analysis time
Reply arrives but await() never returnsThe calling fiber is not inside the runtime event loopCall await() only from within a fiber managed by the runtime — not from a plain PHP script outside $system->run()

Next steps