Type-safe error handling for PHP — chain operations that can fail without exceptions.
composer require hypothesisphp/resultRequirements: PHP 8.2+, zero dependencies.
use Result\Result;
use Result\Success;
use Result\Failure;
// Create results
$success = Result::success('hello');
$failure = Result::failure('something went wrong');
// Chain operations
$result = Result::success(5)
->map(fn (int $v) => $v * 2)
->filter(fn (int $v) => $v > 5, 'too small')
->map(fn (int $v) => $v + 1);
// Unwrap safely
$value = $result->unwrap(); // 11
$value = $result->unwrapOr(0); // 11
$value = $failure->unwrapOr('default'); // 'default'
// Pattern match
$message = $result->match(
onSuccess: fn (int $v) => "Result: {$v}",
onFailure: fn (string $e) => "Error: {$e}",
);Exceptions are unpredictable — they break control flow, hide in call stacks, and make error handling implicit. Result<T, E> makes errors explicit, composable, and type-safe.
Before (exceptions):
try {
$user = $userService->find($id);
$profile = $profileService->load($user);
echo $profile->name;
} catch (UserNotFoundException $e) {
// handle
} catch (ProfileException $e) {
// handle
}After (Result):
$userService->find($id)
->flatMap(fn (User $u) => $profileService->load($u))
->match(
onSuccess: fn (Profile $p) => echo $p->name,
onFailure: fn (string $e) => handle($e),
);| Feature | Description |
|---|---|
| Immutable | All operations return new instances — original never changes |
| Composable | Chain map, flatMap, filter, tap, or, and |
| Pattern Match | match() with exhaustive success/failure handling |
| Collect | Aggregate multiple results with strategies |
| Try/Catch | Wrap throwing code into safe Results |
| Fiber Support | tryAsync() for Fiber-based async operations |
| PSR-3 Logger | Auto-log failures while preserving the chain |
| Serialization | Convert Results to/from arrays and JSON |
| Retry | Built-in retry with configurable attempts and delay |
| Timeout | Execute with timeout protection |
| PHPDoc Generics | Full @template<T, E> annotations for static analysis |
Result::success($value); // Success<T>
Result::failure($error); // Failure<E>
Result::fromNullable($value, $error); // null → Failure
Result::fromCondition($bool, $value, $error); // bool → Success/Failure
Result::try(fn () => risky()); // Throwable → Failure
Result::tryCatch(fn () => risky(), \InvalidArgumentException::class);
Result::tryAsync(fn () => fiberWork()); // Fiber-aware$result->map(fn ($v) => transform($v)); // Transform success value
$result->mapError(fn ($e) => wrap($e)); // Transform error value
$result->flatMap(fn ($v) => validate($v)); // Chain Result-returning operations
$result->filter(fn ($v) => $v > 0, 'err'); // Conditional success$result->unwrap(); // Value or throw
$result->unwrapOr('default'); // Value or default
$result->unwrapOrElse(fn ($e) => calc()); // Value or compute from error
$result->unwrapOrNull(); // Value or null
$result->match(onSuccess: $fn1, onFailure: $fn2); // Pattern match$result->tap(fn ($v) => log($v)); // Inspect without changing
$result->tapError(fn ($e) => log($e)); // Inspect errors
$result->ifSuccess(fn ($v) => notify()); // Execute on success
$result->ifFailure(fn ($e) => alert()); // Execute on failure$result->orElse(fn () => fallback()); // Recovery from failure
$result->or(fn () => alternative()); // Try alternative
$result->and($otherResult); // Chain to next result
// Batch operations
Result::collect([$r1, $r2, $r3]); // All succeed or first failure
Result::collectWith($results, CollectStrategy::ALL_FAILURES);
Result::collectAssoc(['name' => $r1, 'age' => $r2]);
Result::all([$r1, $r2]); // Collect ALL errors on failure
Result::race([fn () => try1(), fn () => try2()]); // First success wins
Result::sequence([fn () => step1(), fn () => step2()]);
Result::partition($results); // {successes: [...], failures: [...]}use function Result\success;
use function Result\failure;
use function Result\try_;
use function Result\from_nullable;
use function Result\from_condition;
$result = success(42);
$result = try_(fn () => json_decode($json, true));
$result = from_nullable($user, 'User not found');use Result\CollectStrategy;
// Stop at first failure (default)
Result::collectWith($results, CollectStrategy::FIRST_FAILURE);
// Collect ALL failures
Result::collectWith($results, CollectStrategy::ALL_FAILURES);
// Return only successes, ignore failures
Result::collectWith($results, CollectStrategy::BEST_EFFORT);Fluent batch aggregation with statistics:
use Result\Collectors\ResultCollector;
$collector = (new ResultCollector())
->add(Result::success(1))
->add(Result::failure('err'))
->add(Result::success(3));
$collector->count(); // 3
$collector->successCount(); // 2
$collector->failureCount(); // 1
$collector->successRatio(); // 0.67
$collector->allSucceeded(); // false
$collector->anyFailed(); // true
$collector->collect(); // Failure (first failure)
$collector->collectAll(); // Failure (all errors)
$collector->collectBestEffort(); // Success([1, 3])
$collector->partition(); // {successes: [1, 3], failures: ['err']}use Result\Interop\PsrResultLoggerBridge;
$bridge = new PsrResultLoggerBridge($logger, 'error');
$result = $userService->find($id);
$bridge->logFailure($result, 'User lookup failed: {error}');
// Or unwrap with automatic logging
$user = $bridge->unwrapOrLog($result, 'Critical: user not found');use Result\Interop\ArrayResultSerializer;
$serializer = new ArrayResultSerializer();
// To array
$array = $serializer->serialize($result);
// ['status' => 'success', 'value' => 42]
// From array
$result = $serializer->deserialize($array);
// JSON support
$json = $serializer->toJson($result);
$result = $serializer->fromJson($json);use Result\Interop\AsyncResultBridge;
// Execute in Fiber
$result = AsyncResultBridge::fromFiber(fn () => heavyWork());
// Concurrent execution
$result = AsyncResultBridge::concurrent([
fn () => Result::success(fetchA()),
fn () => Result::success(fetchB()),
]);
// With timeout
$result = AsyncResultBridge::withTimeout(fn () => slow(), 5000);
// With retry
$result = AsyncResultBridge::withRetry(
fn () => unreliable(),
maxRetries: 3,
delayMs: 100,
);Full API documentation: docs/api-reference.md
- PHP 8.2 or higher
- No external dependencies
composer test # Run tests
composer test:coverage # Run with HTML coverage
composer analyse # PHPStan level 9
composer psalm # Psalm level 1
composer infection # Mutation testing
composer ci # Full CI pipelinePlease see CONTRIBUTING.md for details.
The MIT License (MIT). Please see LICENSE for more information.