Type-safe error handling for PHP — chain operations that can fail without exceptions.

Maintainers

Package info

github.com/HypothesisPHP/Result

pkg:composer/hypothesisphp/result

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.0.0 2026-07-17 10:25 UTC

This package is not auto-updated.

Last update: 2026-07-18 08:45:48 UTC


README

Type-safe error handling for PHP — chain operations that can fail without exceptions.

Installation

composer require hypothesisphp/result

Requirements: PHP 8.2+, zero dependencies.

Quick Start

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}",
);

Why Result?

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),
    );

Features

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

Core Methods

Creating Results

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

Transforming

$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

Extracting

$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

Side Effects

$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

Combining

$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: [...]}

Helper Functions

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');

Collect Strategies

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);

ResultCollector

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']}

PSR-3 Logger Integration

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');

Serialization

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);

Async / Fiber Support

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,
);

API Reference

Full API documentation: docs/api-reference.md

Requirements

  • PHP 8.2 or higher
  • No external dependencies

Testing

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 pipeline

Contributing

Please see CONTRIBUTING.md for details.

License

The MIT License (MIT). Please see LICENSE for more information.