Chaining and Recovery
This group covers the functions that transform the success or failure branch, then move the flow forward.
Quick Map
| Function | What it does |
|---|---|
map | Transforms the success value |
ensure | Converts a false predicate into failure |
mapError | Transforms the failure value |
otherwise | Runs only on failure and may recover |
catchException | Matches Throwable failures by class |
matchError | Finalizes structured ResultError failures by class |
catchError | Recovers or re-fails structured ResultError failures by class |
recover | Converts failure into success |
then | Chains a step with exception capture |
flatMap | Alias of then |
thenUnsafe | Chains a step without exception capture |
map
map(...) transforms the success value and leaves the failure branch unchanged.
map(callable $map): selfInputs:
$map: callback that receives the success value and metadata
Behavior:
- runs only on
Ok - returns a new success result with the mapped value
- preserves metadata
Use it for pure success-value transforms.
Use:
$result = Result::ok(['total' => 42])
->map(fn (array $order) => [...$order, 'tax' => 4.2]);ensure
ensure(...) keeps a success only when the predicate returns true.
ensure(callable $predicate, mixed $error): selfInputs:
$predicate: callback that receives the success value and metadata and returns a boolean$error: failure value or failure factory used when the predicate returnsfalse
Behavior:
- runs only on
Ok - returns the original success when the predicate passes
- converts to failure when the predicate fails
- the error may be a value or a factory callback
Use it for inline validation that should stay in the chain.
Use:
$result = Result::ok(['email' => 'dev@example.com'])
->ensure(fn (array $input) => isset($input['email']), 'Email is required');mapError
mapError(...) transforms the failure value and leaves success unchanged.
mapError(callable $map): selfInputs:
$map: callback that receives the failure value and metadata
Behavior:
- runs only on
Fail - preserves metadata
Use it to normalize failure payloads before handling them further.
Use:
$result = Result::fail('timeout')
->mapError(fn (string $error) => ['code' => 'TIMEOUT', 'message' => $error]);otherwise
otherwise(...) runs only on failure.
otherwise(callable|object|array $next): selfInputs:
$next: callback, action object, or array of steps to run on failure
Behavior:
- a plain return value recovers to success
- a returned
Resultis propagated - a success branch passes through unchanged
Use it when the failure path should stay inside the Result flow.
Use:
$result = Result::fail('timeout')
->otherwise(fn ($error, array $meta) => Result::ok('retry-later', $meta));catchException
catchException(...) handles failure values that are Throwable instances by class.
catchException(array $handlers, ?callable $fallback = null): selfInputs:
$handlers: map of Throwable class to handler callback$fallback: optional callback for unmatched failures
Behavior:
- only
Throwablefailures are class-matched - handlers may return a
Resultor a plain value - a fallback handles unmatched failures when provided
- unmatched failures pass through unchanged when no fallback exists
Use it when exception type determines the next branch.
Use:
$result = Result::fail(new InvalidArgumentException('bad input'))
->catchException([
InvalidArgumentException::class => fn ($error) => Result::fail('normalized'),
]);matchError
matchError(...) finalizes structured ResultError failures by class.
matchError(array $errorHandlers, callable $onSuccess, callable $onUnhandled): mixedBehavior:
- handlers are checked in array order; the first matching class wins
- handler callbacks may accept the error only, or the error plus metadata
onSuccess/onUnhandledmay accept no arguments, the value/error only, or the value/error plus metadata- if the failure is not a
ResultError, or no class matches,onUnhandledruns
Use it when failures are named DataTaggedError subclasses rather than raw strings, arrays, or infrastructure exceptions. Any ResultError implementation is valid; DataTaggedError is simply the common built-in choice.
$message = Result::fail(UserPersistError::from('Unable to save user'))
->matchError(
[UserPersistError::class => fn (UserPersistError $e) => $e->code()],
onSuccess: fn ($value) => 'ok',
onUnhandled: fn ($error) => 'unhandled',
);catchError
catchError(...) recovers or re-fails structured ResultError failures by class.
catchError(array $handlers, ?callable $fallback = null): selfBehavior:
- handlers are checked in array order; the first matching class wins
- handler and fallback callbacks may accept no arguments, the error only, or the error plus metadata
- callbacks may return either a plain recovered success value or a
Result - if no handler matches, fallback is used when provided
- if no handler matches and no fallback exists, the original failure is preserved
Use it when the failure path should stay inside the Result flow.
$result = Result::fail(UserPersistError::from('Unable to save user'))
->catchError([
UserPersistError::class => fn (UserPersistError $e) => 'retry-later',
]);recover
recover(...) converts any failure into success.
recover(callable $fn): selfInputs:
$fn: callback that receives the failure value and metadata
Behavior:
- on
Ok, the original success is preserved - on
Fail, the callback produces the success value
Use it only when you intend to stop carrying the failure branch forward.
Use:
$result = Result::fail('timeout')
->recover(fn ($error, array $meta) => 'fallback');then
then(...) runs a next step only on success and catches exceptions.
then(callable|object|array $next): selfInputs:
$next: callback, invokable object, action object, or array of steps to run on success
Behavior:
- a step may return a plain value or a
Result - thrown exceptions become failure
- arrays of steps are run in order
- callable arrays such as
[$service, 'handle']stay one step - objects with
__invoke,handle, orexecuteare supported
Use it for the default success-path pipeline.
Use:
$result = Result::ok(['total' => 42])
->then(fn (array $order) => Result::ok([...$order, 'tax' => 4.2]));flatMap
flatMap(...) is an alias of then(...).
flatMap(callable $fn): selfUse it if you prefer monadic naming.
Use:
$result = Result::ok(['total' => 42])
->flatMap(fn (array $order) => Result::ok([...$order, 'tax' => 4.2]));thenUnsafe
thenUnsafe(...) runs a next step only on success, but does not catch exceptions.
thenUnsafe(callable|object $next): selfInputs:
$next: callback or object to invoke on success
Behavior:
- plain return values are wrapped as success
Resultreturn values are propagated- thrown exceptions bubble up
Use it when exception bubbling is the desired boundary behavior.
Use:
$result = Result::ok(['total' => 42])
->thenUnsafe(fn (array $order) => [...$order, 'tax' => 4.2]);