How to use the runtime without a full actor system
When you need async primitives — concurrent requests, parallel fetches, fire-and-forget work — but do not need actor supervision trees, you can use Future and the fiber runtime directly. This avoids the overhead of an ActorSystem and is the right fit for one-off scripts, CLI commands, and integration glue code.
Solution
<?php
declare(strict_types=1);
use Monadial\Nexus\Runtime\Async\Future;
use Monadial\Nexus\Runtime\Async\FutureSlot;
use Monadial\Nexus\Runtime\Async\LazyFutureSlot;
use Monadial\Nexus\Runtime\Duration;
use Monadial\Nexus\Runtime\Fiber\FiberRuntime;
$runtime = new FiberRuntime();
// Spawn two concurrent tasks
$futureA = new Future(new LazyFutureSlot(static fn() => fetchRemoteData('https://api.example.com/a')));
$futureB = new Future(new LazyFutureSlot(static fn() => fetchRemoteData('https://api.example.com/b')));
// Wait for both
$combined = Future::all(['a' => $futureA, 'b' => $futureB]);
$runtime->run();
$results = $combined->await();
For the simpler case of running a single async closure, the standalone runtime documented in runtimes/standalone.md provides a higher-level helper.
How it works
Future<R> is a handle to a pending result. It wraps a FutureSlot — the internal resolution mechanism that each runtime implements using its own suspension primitive. FiberRuntime suspends the calling fiber via Fiber::suspend() when await() is called and resumes it when the slot is resolved.
Future::all() creates a combined future that resolves when all input futures resolve. Internally it uses LazyFutureSlot, which defers execution until await() is called — no fibers are spawned until the runtime starts processing.
Future::resolved($value) creates an already-completed future, useful for testing and for composing with flatMap() without actually suspending.
Variations
Transforming a future result
Future::map() transforms the resolved value lazily — the callback runs when the future resolves, not when map() is called:
<?php
declare(strict_types=1);
use Monadial\Nexus\Runtime\Async\Future;
$rawFuture = fetchOrderFuture($orderId);
$summaryFuture = $rawFuture->map(
static fn(Order $order) => new OrderSummary($order->id, $order->total),
);
$summary = $summaryFuture->await();
Chaining dependent futures
Future::flatMap() chains a second async operation that depends on the first result:
<?php
declare(strict_types=1);
use Monadial\Nexus\Runtime\Async\Future;
$orderFuture = fetchOrderFuture($orderId);
$invoiceFuture = $orderFuture->flatMap(
static fn(Order $order) => generateInvoiceFuture($order),
);
$invoice = $invoiceFuture->await();
Cancellation
Futures can be cancelled. Register a cancellation callback to clean up resources:
<?php
declare(strict_types=1);
use Monadial\Nexus\Runtime\Async\Future;
$future = fetchWithTimeoutFuture($url);
$future->onCancel(static fn() => cleanupConnection());
// Cancel if it takes too long
if ($tookTooLong) {
$future->cancel();
}
Caveats
Future::await() suspends the current fiber. If called from the main thread (outside a fiber), it throws because there is no fiber to suspend. Wrap standalone code in $runtime->spawn(fn() => ...) or use $runtime->run() to drive the fiber scheduler.
LazyFutureSlotdefers execution. The closure passed toLazyFutureSlotdoes not run untilawait()is called. If the future is never awaited, the work never runs.FutureExceptioncovers timeout and cancellation.FutureTimeoutExceptionandFutureCancelledExceptionboth extendFutureException. Catch the base class unless you need to distinguish them.Future::all()fails fast. If any input future fails, the combined future fails immediately with the first error. Remaining futures are not cancelled — they continue running but their results are discarded.- No supervision. Standalone futures have no supervisor. Unhandled exceptions in a
LazyFutureSlotpropagate to theawait()caller asFutureException. Add your owntry/catcharoundawait()when failure handling matters.
Related
- Standalone runtime — higher-level helpers for running closures as fibers
- Ask pattern —
ask()returns aFuture; the concepts are identical - Fiber runtime — how the fiber scheduler drives future resolution