A Result type implementation for PHP inspired by Rust's Result<T, E> type.
This library provides a robust way to handle operations that might fail, without relying on exceptions. It encourages explicit error handling by making the error case part of the return type, so failures are visible in function signatures instead of being hidden control flow.
composer require valbeat/result- PHP 8.3 or higher (tested on PHP 8.3, 8.4 and 8.5)
- Composer
use Valbeat\Result\Ok;
use Valbeat\Result\Err;
// Creating Results
$success = new Ok(42);
$failure = new Err("Something went wrong");
// Pattern matching with the match() method
$message = $success->match(
ok: fn($value) => "Success: $value",
err: fn($error) => "Error: $error"
);
echo $message; // "Success: 42"
// Checking if a Result is Ok or Err
if ($success->isOk()) {
echo "Operation succeeded!";
}
if ($failure->isErr()) {
echo "Operation failed!";
}
// Unwrapping values (throws exception on error)
$value = $success->unwrap(); // 42
// $failure->unwrap(); // throws UnwrapException
// Safe unwrapping with default values
$value = $failure->unwrapOr(0); // 0
$value = $failure->unwrapOrElse(fn($err) => strlen($err)); // 20// Map over success values
$result = (new Ok(5))
->map(fn($x) => $x * 2)
->map(fn($x) => $x + 1);
echo $result->unwrap(); // 11
// Map over error values
$result = (new Err("error"))
->mapErr(fn($e) => strtoupper($e));
echo $result->unwrapErr(); // "ERROR"// Chain operations that might fail
$result = (new Ok(10))
->andThen(fn($x) => $x > 5 ? new Ok($x * 2) : new Err("Too small"))
->andThen(fn($x) => new Ok($x + 5));
echo $result->unwrap(); // 25
// Short-circuit on first error
$result = (new Ok(2))
->andThen(fn($x) => $x > 5 ? new Ok($x * 2) : new Err("Too small"));
echo $result->unwrapErr(); // "Too small"Each step in a chain may fail with a different error type. The error types are composed into a union, so PHPStan tracks every error the chain can produce:
final class ValidationError {}
final class NotFoundError {}
/** @return Result<int, ValidationError> */
function validateUserId(string $raw): Result
{
return ctype_digit($raw) ? new Ok((int) $raw) : new Err(new ValidationError());
}
/** @return Result<string, NotFoundError> */
function findUserNameById(int $id): Result
{
return $id === 42 ? new Ok('Alice') : new Err(new NotFoundError());
}
// PHPStan infers Result<string, ValidationError|NotFoundError>
$userName = validateUserId('42')->andThen(findUserNameById(...));
echo $userName->unwrap(); // "Alice"// Use first Ok value
$result = (new Err("first error"))
->or(new Err("second error"))
->or(new Ok(42));
echo $result->unwrap(); // 42
// Use first Ok or call function
$result = (new Err("error"))
->orElse(fn($e) => new Ok(strlen($e)));
echo $result->unwrap(); // 5// Inspect values without consuming the Result
$result = (new Ok(42))
->inspect(fn($x) => print("Value is: $x\n"))
->map(fn($x) => $x * 2);
// Inspect errors
$result = (new Err("oops"))
->inspectErr(fn($e) => error_log("Error occurred: $e"));use Valbeat\Result\Results;
// Wrap exception-throwing code: Ok on success, Err<Throwable> on throw
$result = Results::try(fn() => json_decode($raw, flags: JSON_THROW_ON_ERROR));
// Combine many Results: Ok with all values, or the first Err
$result = Results::combine([new Ok(1), new Ok(2), new Ok(3)]);
echo implode(',', $result->unwrap()); // "1,2,3"
// Flatten a nested Result<Result<T, E2>, E1> into Result<T, E1|E2>
$result = Results::flatten(new Ok(new Ok(42)));
echo $result->unwrap(); // 42This library is designed to be used with PHPStan at level max and leans on several of its generics features:
- Sealed interface —
Resultis annotated with@phpstan-sealed Ok|Err, so PHPStan knowsOkandErrare the only implementations. Amatch (true)overinstanceofchecks is recognized as exhaustive, and theelsebranch of aninstanceof Okcheck narrows toErr. Note thatinstanceofnarrowing loses the type arguments (a known PHPStan limitation:Result<int, E>narrows to plainOk, sounwrap()becomesmixed), so useinstanceofin amatch (true)purely for exhaustiveness. When you also need the values, thematch()method handles both cases and keepsT/E(it requires both anokand anerrarm), or narrow withisOk()/isErr(). These two goals are a trade-off in PHPStan 2.2.2: enforced exhaustiveness — where adding a newResultvariant would turn every unhandled site into an analysis error — comes only frominstanceofin amatch (true), which is exactly the form that drops the type arguments. Thematch()method andisOk()/isErr()keep the generics but are not checked against variant additions (isOk()/isErr()arms in amatch (true)also need adefault). So per call site you currently pick one: enforced exhaustiveness or preserved generics. (In practiceResultis fixed atOk|Err, so thematch()method covering both is total for all real cases.) - Covariant type parameters —
TandEare declared@template-covariant, soOk<T>(which isResult<T, never>) andErr<E>(which isResult<never, E>) are assignable to anyResult<T, E>. A function declared to returnResult<User, DbError>can simplyreturn new Ok($user);. - Error-type composition —
andThen()/and()widen the error channel toE|FandorElse()/or()widen the success channel toT|U, so chains that mix failure types stay precisely typed. (This deliberately diverges from Rust, whoseand/orfamily keeps the other channel's type fixed.) - Type narrowing —
isOk()/isErr()narrow$resulttoOk<T>/Err<E>via@phpstan-assert-if-true;unwrap()/unwrapErr()use conditional return types (neveron the impossible side), andunwrapOr()/unwrapOrElse()resolve toTonOkand to the default's type onErr. - Exhaustive error matching — when the error type
Eis a nativeenumor a@phpstan-sealedunion, the error value can be matched exhaustively (over the enum cases, orinstanceofarms for a sealed union) and PHPStan enforces it — a missing case becomes an analysis error. Reach the error throughisErr()+unwrapErr(), or thematch()method'serrarm; narrowing withinstanceof ErrdropsEtomixedand loses the enum/sealed type (the sameinstanceoflimitation as above). As long as the error classes are themselves non-generic, their owninstanceofchecks have no type arguments to lose (unlike the genericOk/Err). - Precise concrete receivers — when the receiver is statically
Ok<T>orErr<E>, no-op methods keep their exact type ($ok->orElse(...)staysOk<T>,$err->andThen(...)staysErr<E>) instead of widening to a union.
Note: because the templates are covariant, PHPStan preserves constant value types
(new Ok(10) is Ok<10>, not Ok<int>). Type a variable or parameter as int
if you want the widened type.
All Result types (both Ok and Err) implement these methods:
isOk(): bool- Returns true if the Result is OkisOkAnd(callable $fn): bool- Returns true if the Result is Ok and the predicate returns trueisErr(): bool- Returns true if the Result is ErrisErrAnd(callable $fn): bool- Returns true if the Result is Err and the predicate returns true
unwrap(): mixed- Returns the success value or throws UnwrapException (extends LogicException)unwrapErr(): mixed- Returns the error value or throws UnwrapException (extends LogicException)expect(string $message): mixed- Returns the success value or throws UnwrapException with the given message and a summary of the error valueexpectErr(string $message): mixed- Returns the error value or throws UnwrapException with the given message and a summary of the success valueunwrapOr(mixed $default): mixed- Returns the success value or a defaultunwrapOrElse(callable $fn): mixed- Returns the success value or computes it from the error
map(callable $fn): Result- Maps a Result<T, E> to Result<U, E> by applying a function to the success valuemapErr(callable $fn): Result- Maps a Result<T, E> to Result<T, F> by applying a function to the error valuemapOr(mixed $default, callable $fn): mixed- Maps the success value or returns a defaultmapOrElse(callable $defaultFn, callable $fn): mixed- Maps the success value or computes a default from the error
and(Result $res): Result- Returns the second Result if the first is Ok, otherwise returns the first ErrandThen(callable $fn): Result- Chains another operation that returns a Resultor(Result $res): Result- Returns the first Ok or the second Result if the first is ErrorElse(callable $fn): Result- Returns the first Ok or calls a function with the error to produce a Result
inspect(callable $fn): Result- Calls a function with the success value if OkinspectErr(callable $fn): Result- Calls a function with the error value if Err
match(callable $ok, callable $err): mixed- Pattern match on the Result
Results::try(callable $fn): Result- Runs a callable and wraps the outcome: Ok with the return value, or Err with the thrown ThrowableResults::combine(iterable $results): Result- Combinesiterable<Result<T, E>>intoResult<list<T>, E>, short-circuiting on the first ErrResults::flatten(Result $result): Result- FlattensResult<Result<T, E2>, E1>intoResult<T, E1|E2>
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.