Result

composer require php-standard-library/result

The Result component represents a computation that either succeeded or failed. Instead of letting exceptions propagate uncontrolled, you capture the outcome as a value that can be inspected, transformed, and passed around.

This is useful when you want to defer error handling, aggregate results from multiple operations, or build transformation pipelines without scattering try/catch blocks throughout your code.

Design

Usage

Wrapping Operations

The wrap() function executes a closure and captures the outcome:

use Psl\Json;
use Psl\Result;
use Psl\Type;

$result = Result\wrap(
    /** @return array<string, string> */
    fn(): array => Json\typed('{"name":"Alice"}', Type\dict(Type\string(), Type\string())),
);

if ($result->isSucceeded()) {
    $data = $result->getResult();
} else {
    $error = $result->getThrowable();
}

// Get the value with a fallback
$data = $result->unwrapOr([]);

Transforming Results

map() transforms a successful value, leaving failures untouched. catch() recovers from failures, leaving successes untouched:

use Psl\File;
use Psl\Result;
use Psl\Str;

$lines = Result\wrap(fn() => File\read(__FILE__))
    ->map(fn(string $content) => Str\split($content, "\n"))
    ->catch(fn(Throwable $_e) => []);

// Success: maps content to lines
// Failure: recovers with empty array

then() handles both cases at once, returning a new ResultInterface:

use Psl\Json;
use Psl\Result;

$normalized = Result\wrap(fn() => Json\decode('{"name":"Alice","age":30}'))
    ->then(fn(mixed $data) => ['status' => 'ok', 'data' => $data], fn(Throwable $e) => ['error' => $e->getMessage()]);

// $normalized is ResultInterface<array>, still wrapped

Pattern Matching with proceed()

The proceed() method unwraps the result by calling the appropriate closure and returning the value directly (not wrapped in a Result):

use Psl\Iter;
use Psl\Json;
use Psl\Result;
use Psl\Type;

$message = Result\wrap(fn(): array => Json\typed('{"a":1,"b":2,"c":3}', Type\dict(Type\string(), Type\int())))
    ->proceed(
        fn(array $data): string => 'Parsed: ' . Iter\count($data) . ' items',
        fn(Throwable $e): string => 'Failed: ' . $e->getMessage(),
    );

Cleanup with always()

Run cleanup logic regardless of outcome:

use Psl\File;
use Psl\Filesystem;
use Psl\Result;

$temp = Filesystem\create_temporary_file();
File\write($temp, 'temporary data');

$result = Result\wrap(fn() => File\read($temp))->always(fn() => Filesystem\delete_file($temp));

Convenience Functions

try_catch() wraps and recovers in a single step:

use Psl\Json;
use Psl\Result;

$_ = Result\try_catch(fn() => Json\decode('not valid json'), fn(Throwable $_e) => []);

reflect() converts a throwing closure into one that returns a Result:

use Psl\Json;
use Psl\Result;

$safeParse = Result\reflect(fn() => Json\decode('{"key":"value"}'));
$result = $safeParse(); // ResultInterface, never throws

Collecting Statistics

When processing batches, collect_stats() summarizes outcomes:

use Psl\Json;
use Psl\Result;
use Psl\Vec;

$inputs = ['{"a":1}', 'invalid', '{"b":2}', '{bad}', '{"c":3}'];

$results = Vec\map($inputs, fn(string $input) => Result\wrap(fn() => Json\decode($input)));

$stats = Result\collect_stats($results);
$stats->total(); // total number of results
$stats->succeeded(); // number of successes
$stats->failed(); // number of failures

When to Use Result

See src/Psl/Result/ for the full API.