PHPackages                             iak/result - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. iak/result

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

iak/result
==========

A return type for operations that can fail — make every failure visible, typed, and impossible to ignore

v1.0.0(1mo ago)00MITPHPPHP ^8.2CI passing

Since Jul 8Pushed 1mo agoCompare

[ Source](https://github.com/iaK/result)[ Packagist](https://packagist.org/packages/iak/result)[ Docs](https://github.com/iaK/result)[ RSS](/packages/iak-result/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (4)Versions (2)Used By (0)

Result
======

[](#result)

[![Latest Version on Packagist](https://camo.githubusercontent.com/69e570235f779d881f909dbfeae9da57ea720e62be0bbcc3f01d6d72eef8d1be/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f69616b2f726573756c742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/iak/result)[![GitHub Tests Action Status](https://camo.githubusercontent.com/f914ca73cffb8c632f0c78af66b32916b9d013dfeaa4494b3172ce7de237e898/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f69614b2f726573756c742f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/iaK/result/actions?query=workflow%3ATests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/743543c068740b8f9de13eb3223bcc5e5e9ebbabe45aff54bf1c7ce6a77d0254/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f69614b2f726573756c742f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/iaK/result/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/4e68060471a54f7d8076392caeb6f4671a3315bb8566cf2f1046a58db7c42922/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f69616b2f726573756c742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/iak/result)

A `Result` is a return type for operations that can fail. Instead of throwing an exception for an outcome you fully expect — an expired card, an empty stock, a closed kitchen — you return it. For example, check out the following code:

```
return $placeOrder->handle($cart, $address)
    ->chain(fn (Order $order) => $charge->handle($order, $card))
    ->match(
        success: fn (Receipt $receipt) => response()->json($receipt),
        failure: fn (OrderError|PaymentError $error) => response()->json(['error' => $error->message()], 422),
    );
```

Two operations that can each fail, one place where failure is handled, and not a try/catch in sight. Every possible outcome is right there in the types — your IDE sees them, your static analyser sees them, and the next developer sees them.

Installation
------------

[](#installation)

```
composer require iak/result
```

That's it. Results require PHP 8.2+ and nothing else — no framework, no configuration, no service provider.

Your First Result
-----------------

[](#your-first-result)

Let's build something real: placing an order. You've probably written this controller a hundred times:

```
try {
    $order = $placeOrder->handle($request->cart(), $request->address());
} catch (KitchenClosedException) {
    return back()->withErrors(['order' => 'The kitchen is closed right now.']);
} catch (OutOfStockException) {
    return back()->withErrors(['order' => 'Some items are out of stock.']);
} catch (DeliveryUnavailableException) {
    return back()->withErrors(['order' => "We don't deliver to this address yet."]);
}

return redirect()->route('orders.show', $order);
```

It works — until someone adds a fourth failure mode to `PlaceOrder` and this controller quietly starts responding with 500s. Nothing in `handle()`'s signature says what it throws, and nothing checks that you caught it all.

Let's rebuild it with a Result. First, give every failure a name. An enum is perfect for this, and it gives the error messages a home too:

```
enum OrderError
{
    case KitchenClosed;
    case OutOfStock;
    case DeliveryUnavailable;

    public function message(): string
    {
        return match ($this) {
            self::KitchenClosed       => 'The kitchen is closed right now.',
            self::OutOfStock          => 'Some items are out of stock.',
            self::DeliveryUnavailable => "We don't deliver to this address yet.",
        };
    }
}
```

Next, instead of throwing, return the outcome — either way:

```
use Iak\Result\Result;

class PlaceOrder
{
    /** @return Result */
    public function handle(Cart $cart, Address $address): Result
    {
        if (! $this->kitchen->isOpen()) {
            return Result::failure(OrderError::KitchenClosed);
        }

        if (! $cart->allItemsAvailable()) {
            return Result::failure(OrderError::OutOfStock);
        }

        if (! $this->zones->covers($address)) {
            return Result::failure(OrderError::DeliveryUnavailable);
        }

        return Result::success(Order::create($cart, $address));
    }
}
```

Notice the docblock: `Result`. That one line now documents every way this operation can end. No source-diving, no tribal knowledge.

Finally, the controller shrinks to a single expression:

```
public function store(StoreOrderRequest $request, PlaceOrder $placeOrder)
{
    return $placeOrder->handle($request->cart(), $request->address())->match(
        success: fn (Order $order) => redirect()->route('orders.show', $order),
        failure: fn (OrderError $error) => back()->withErrors(['order' => $error->message()]),
    );
}
```

Note

**Where did the try/catch go?** There's nothing to catch — failure is just a return value now. And unlike a catch block, `match()` can't be forgotten: it's the only way to get at the order, and it requires both arms.

And when someone adds that fourth failure mode? They add an enum case, the `match` inside `message()` immediately demands a message for it, and every consumer of the error gets flagged by static analysis. The failure mode is born handled.

That's the whole pattern. Everything else in this package is convenience on top of it.

Available Methods
-----------------

[](#available-methods)

[all](#all) · [chain](#chain) · [error](#error) · [expect](#expect) · [expectError](#experror) · [failure](#failure) · [isFailure](#isfailure) · [isSuccess](#issuccess) · [map](#map) · [mapError](#maperror) · [match](#match) · [orElse](#orelse) · [success](#success) · [tap](#tap) · [tapError](#taperror) · [value](#value) · [valueOr](#valueor) · [valueOrElse](#valueorelse)

Method Listing
--------------

[](#method-listing)

### `success()`

[](#success)

The static `success` method wraps a value in a successful result:

```
return Result::success($order);
```

You may call it without arguments when the operation has nothing meaningful to return — "it worked" is the whole message:

```
return Result::success();
```

### `failure()`

[](#failure)

The static `failure` method wraps an error in a failed result. The error may be anything: an enum, a value object carrying context, a string, or an exception if you already have one:

```
return Result::failure(OrderError::OutOfStock);
return Result::failure(new AddressOutsideZone($address, $nearestZone));
return Result::failure($caughtException);
```

### `all()`

[](#all)

The static `all` method combines a collection of results into a single result. If every result succeeded, you get one success holding all the values with their keys preserved. If any failed, you get the first failure back, untouched:

```
$result = Result::all([
    'order'   => $placeOrder->handle($cart, $address),
    'invoice' => $createInvoice->handle($cart),
]);

// success: ['order' => Order, 'invoice' => Invoice]
// failure: whichever failed first
```

You may pass any iterable. Iteration stops at the first failure, so a lazy generator won't do more work than necessary.

### `isSuccess()`

[](#issuccess)

The `isSuccess` method determines whether the operation succeeded:

```
if ($result->isSuccess()) {
    $order = $result->value(); // safe — and your static analyser agrees
}
```

### `isFailure()`

[](#isfailure)

The `isFailure` method is the mirror of [`isSuccess`](#issuccess). It shines in guard clauses, keeping the happy path flat:

```
if ($result->isFailure()) {
    return back()->withErrors(['order' => $result->error()->message()]);
}

$order = $result->value();
```

Note

Prefer these methods over `instanceof` checks. Static analysers can't carry the value and error types through a bare `instanceof`, but `isSuccess()` and `isFailure()` keep them intact.

### `value()`

[](#value)

The `value` method returns the success value:

```
$order = $result->value();
```

If the result is a failure, `value` throws a `ResultException`. An unguarded call is therefore an assertion — "this can't fail here, and if I'm wrong I want to hear about it." When you're not asserting, guard with [`isFailure`](#isfailure) first or reach for [`valueOr`](#valueor).

### `error()`

[](#error)

The `error` method returns the error value, throwing a `ResultException` if the result is actually a success. You'll use it after a guard, and all over your tests:

```
expect($result->error())->toBe(OrderError::OutOfStock);
```

### `expect()`

[](#expect)

The `expect` method works like [`value`](#value), but the exception carries your message — so when the impossible happens, whoever's on call knows what you were assuming:

```
$order = $result->expect('cart was validated in the previous step');
```

### `expectError()`

[](#expecterror)

The `expectError` method is [`expect`](#expect) for the error side:

```
$error = $result->expectError('the gateway was stubbed to fail in this test');
```

### `valueOr()`

[](#valueor)

The `valueOr` method returns the success value, or your fallback if the operation failed — for when you don't care why:

```
$eta = $estimateDelivery->handle($address)->valueOr(45);
```

### `valueOrElse()`

[](#valueorelse)

The `valueOrElse` method computes the fallback from the error, and only when it's actually needed:

```
$eta = $estimateDelivery->handle($address)
    ->valueOrElse(fn (EstimateError $error) => $error->conservativeGuess());
```

### `map()`

[](#map)

The `map` method transforms the success value without unpacking the result. A failure passes through untouched:

```
$result->map(fn (Order $order) => $order->total);
// Result becomes Result
```

### `mapError()`

[](#maperror)

The `mapError` method transforms the error instead. It's the tool for translating low-level errors into your domain's language at a boundary:

```
$gateway->charge($card)
    ->mapError(fn (GatewayError $error) => PaymentError::fromGateway($error));
```

### `tap()`

[](#tap)

The `tap` method runs a side effect on the success value — logging, metrics, notifications — and hands the result back unchanged:

```
return $placeOrder->handle($cart, $address)
    ->tap(fn (Order $order) => Log::info('order placed', ['id' => $order->id]));
```

### `tapError()`

[](#taperror)

The `tapError` method does the same for the failure side. Together they let you observe a pipeline without interrupting it:

```
return $placeOrder->handle($cart, $address)
    ->tap(fn (Order $order) => Log::info('order placed', ['id' => $order->id]))
    ->tapError(fn (OrderError $error) => Metrics::increment('orders.rejected'));
```

### `chain()`

[](#chain)

The `chain` method pipes the success value into the next operation that can itself fail. The first failure short-circuits everything after it, so a whole workflow needs exactly one failure handler:

```
return $placeOrder->handle($cart, $address)                       // Result
    ->chain(fn (Order $order) => $charge->handle($order, $card))  // Result
    ->match(
        success: fn (Receipt $receipt) => response()->json($receipt),
        failure: fn (OrderError|PaymentError $error) => response()->json(['error' => $error->message()], 422),
    );
```

Notice how the error types stack up: add a step, and every handler downstream is made aware of what it might have to deal with.

Note

**Why isn't this called `then()`?** Promise libraries — Guzzle, and therefore Laravel's `Http::async()`, and ReactPHP — treat any object with a public `then()` method as a promise and try to resolve it. A Result named that way would break inside promise pipelines, so it's `chain()`.

### `orElse()`

[](#orelse)

The `orElse` method is recovery: on failure, try something else that can itself succeed or fail. A success passes straight through:

```
$receipt = $chargeCard->handle($order, $card)
    ->orElse(fn (PaymentError $error) => $chargeWallet->handle($order));
```

### `match()`

[](#match)

The `match` method handles both outcomes in one expression. Both arms are required — the failure path can't be forgotten:

```
return $result->match(
    success: fn (Order $order) => redirect()->route('orders.show', $order),
    failure: fn (OrderError $error) => back()->withErrors(['order' => $error->message()]),
);
```

Handling Different Error Types
------------------------------

[](#handling-different-error-types)

Once you start chaining, the error side becomes a union — `OrderError|PaymentError`— and you may want to treat them differently. PHP's own `match` has you covered:

```
$result->match(
    success: fn (Receipt $receipt) => response()->json($receipt),
    failure: fn (OrderError|PaymentError $error) => match (true) {
        $error instanceof OrderError   => back()->withErrors(['order' => $error->message()]),
        $error instanceof PaymentError => $this->redirectToPaymentRetry($error),
    },
);
```

Static analysers check that inner `match` against the union — add a third error type to the chain and this exact spot gets flagged until you handle it.

If every error is handled the same way, skip the dispatch entirely and give your errors a shared interface:

```
interface DomainError
{
    public function message(): string;
}

// enum OrderError implements DomainError { ... }
// enum PaymentError implements DomainError { ... }

failure: fn (DomainError $error) => back()->withErrors(['order' => $error->message()]),
```

And when a union grows past comfort, normalize early: use [`mapError`](#maperror) at each boundary so downstream code only ever sees one error type.

Good to Know
------------

[](#good-to-know)

- **Results are immutable.** Transformations never modify a result in place — they hand you a new one (or the same one, untouched).
- **Results compare structurally.** `Result::success(1) == Result::success(1)`is `true`.
- **Results serialize.** As long as the contained value serializes, a result survives caches and queues.
- **Results are sealed.** `Success` and `Failure` are final, so `isFailure() === false` always means success.
- **`ResultException` is the only exception this package throws** — from [`value`](#value), [`error`](#error), [`expect`](#expect) and [`expectError`](#experror) on the wrong variant. It carries the offending value on `->value`, and when the error is itself a `Throwable`, you'll find it chained on `->getPrevious()` with its stack trace intact.

Development
-----------

[](#development)

```
composer test      # Pest
composer analyse   # PHPStan, including the type-inference fixtures in types/
composer format    # Pint
```

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance91

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

49d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/9f8e167637ace3c09cf95c25f7d826a2352f06a7bd47d512f2ee5ed484840a27?d=identicon)[iaK](/maintainers/iaK)

---

Top Contributors

[![iaK](https://avatars.githubusercontent.com/u/2571644?v=4)](https://github.com/iaK "iaK (32 commits)")

---

Tags

phpPHPStanresultResult-Typeerror handling

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/iak-result/health.svg)

```
[![Health](https://phpackages.com/badges/iak-result/health.svg)](https://phpackages.com/packages/iak-result)
```

###  Alternatives

[graham-campbell/result-type

An Implementation Of The Result Type

554427.4M14](/packages/graham-campbell-result-type)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
