Promise
The Promise component provides a promise interface for deferred computations. A promise represents a value that will be available in the future -- either resolved with a success value or rejected with an exception. You attach callbacks to react to the outcome without blocking.
Design
PromiseInterface<T> defines four methods for composing asynchronous operations:
then()-- Handle both success and failure in one callmap()-- Transform the resolved valuecatch()-- Recover from rejectionalways()-- Run cleanup logic regardless of outcome
Every method returns a new PromiseInterface, so you can chain them into pipelines.
Usage
Transforming a Resolved Value
map() applies a function to the resolved value. If the promise is rejected, the callback is skipped and the rejection propagates:
use Psl\Async;
use Psl\Str;
/** @var Async\Awaitable<string> $promise */
$promise = Async\run(static fn() => 'hello');
$upper = $promise->map(fn(string $body) => Str\uppercase($body));
// If $promise resolves with 'hello', $upper resolves with 'HELLO'
// If $promise is rejected, $upper is also rejected with the same exception
$upper->await(); // 'HELLO'
Recovering from Failure
catch() attaches a callback that is only invoked when the promise is rejected. The returned value becomes the new resolved value:
use Psl\Async;
/** @var Async\Awaitable<string> $promise */
$promise = Async\run(static fn() => throw new Exception('oh no'));
$safe = $promise->catch(fn(Throwable $_) => 'default response');
// If $promise is rejected, $safe resolves with 'default response'
// If $promise succeeds, $safe resolves with the original value
$safe->await(); // 'default response'
Handling Both Cases with then()
then() is a shortcut for chaining map() and catch():
use Psl\Async;
/** @var Async\Awaitable<string> $promise */
$promise = Async\run(static fn() => 'hello world');
$result = $promise->then(fn(string $value) => ['value' => $value], fn(\Throwable $e) => ['error' => $e->getMessage()]);
$result->await(); // ['value' => 'hello world']
This is equivalent to:
use Psl\Async;
/** @var Async\Awaitable<string> $promise */
$promise = Async\run(static fn() => 'hello world');
$result = $promise->map(fn(string $value) => ['value' => $value])->catch(fn(\Throwable $e) => [
'error' => $e->getMessage(),
]);
$result->await(); // ['value' => 'hello world']
Cleanup with always()
always() runs a callback when the promise settles, regardless of whether it resolved or was rejected. The promise's value passes through unchanged (unless the callback throws):
use Psl\Async;
use Psl\IO;
use Psl\Str;
/** @var Async\Awaitable<string> $promise */
$promise = Async\run(static fn() => 'hello');
$promise
->map(fn(string $content) => Str\uppercase($content))
->always(fn() => IO\write_error_line('cleanup complete'))
->await();
Building Pipelines
Because every method returns a new promise, you can compose multi-step async workflows:
use Psl\Async;
use Psl\IO;
use Psl\Json;
use Psl\Type;
/** @var Async\Awaitable<string> $promise */
$promise = Async\run(static fn() => '{"name": "psl", "version": "3.0"}');
$processed = $promise
->map(fn(string $raw) => Json\decode($raw))
->map(
fn(mixed $data) => Type\shape([
'name' => Type\string(),
'version' => Type\string(),
])->coerce($data),
)
->map(fn(array $valid) => ['project' => $valid['name'], 'v' => $valid['version']])
->catch(fn(\Throwable $e) => ['error' => $e->getMessage()])
->always(fn() => IO\write_error_line('pipeline complete'));
$processed->await();
When to Use Promise
- Async operations -- React to the outcome of concurrent tasks
- Pipeline composition -- Chain transformations that may fail at any step
- Error boundaries -- Catch and recover from failures at specific points in the chain
- Resource cleanup -- Use
always()to guarantee cleanup runs
See src/Psl/Promise/ for the full API.