martyrer / throwless-php
A PHP library for exception-free error handling using the Result monad pattern
Requires
- php: ^8.2
Requires (Dev)
- laravel/pint: ^1.22
- pestphp/pest: ^3.8
- pestphp/pest-plugin-type-coverage: ^3.5
- phpstan/phpstan: ^2.1
- rector/rector: ^2.0
README
A PHP implementation of the Result type pattern, inspired by Rust's Result
and JavaScript's neverthrow
library. This library provides a type-safe way to handle errors without relying on exceptions, making your code more predictable and easier to reason about.
Features
- Type-safe error handling without exceptions
- Immutable Result type with
Ok
andErr
variants - Comprehensive set of methods for working with Results
- Pure functional programming approach
- PHP 8.2+ with strict typing support
Installation
composer require martyrer/throwless-php
Available Methods
-
isOk()
- Check if the Result contains a success valueisErr()
- Check if the Result contains an error valueisOkAnd(callable $fn)
- Executes the closure and returns its result as a boolean if the result is OkisErrAnd(callable $fn)
- Executes the closure and returns its result as a boolean if the result is Err
-
map(callable $fn)
- Transform the success valuemapErr(callable $fn)
- Transform the error valueandThen(callable $fn)
- Chain Result-returning operations
-
match(callable $okFn, callable $errFn)
- Handle both success and error cases
-
inspect(callable $fn)
- Inspect the success valueinspectErr(callable $fn)
- Inspect the error value
-
unwrap()
- Get the success value or throwunwrapErr()
- Get the error value or throwunwrapOr(mixed $default)
- Get the success value or a defaultunwrapOrElse(callable $fn)
- Get the success value or compute a defaultunwrapOrDefault(DefaultValueProvider $provider)
- Get the success value or type's defaultexpect(string $msg)
- Get the success value or throw with custom messageexpectErr(string $msg)
- Get the error value or throw with custom messageunwrapUnchecked()
- Get the success value without checking (unsafe)unwrapErrUnchecked()
- Get the error value without checking (unsafe)
Basic Usage
use Martyrer\Throwless\Ok; use Martyrer\Throwless\Err; use Martyrer\Throwless\Result; // Creating Results $success = new Ok(42); // Ok<int, mixed> $failure = new Err("something went wrong"); // Err<mixed, string> // Checking Result type $success->isOk(); // true $success->isErr(); // false $failure->isOk(); // false $failure->isErr(); // true // Unwrapping values $success->unwrap(); // 42 $failure->unwrapErr(); // "something went wrong" // Using default values $success->unwrapOr(0); // 42 $failure->unwrapOr(0); // 0
Core Methods
Type Checking
isOk
Check if the Result contains a success value:
$result = new Ok(42); $result->isOk(); // true $error = new Err("failed"); $error->isOk(); // false
isErr
Check if the Result contains an error value:
$result = new Ok(42); $result->isErr(); // false $error = new Err("failed"); $error->isErr(); // true
isOkAnd
Check if Result is Ok and the value matches a predicate:
$result = new Ok(42); $isEven = $result->isOkAnd(fn($x) => $x % 2 === 0); // true $isNegative = $result->isOkAnd(fn($x) => $x < 0); // false
isErrAnd
Check if Result is Err and the error matches a predicate:
$error = new Err("invalid input"); $isInputError = $error->isErrAnd(fn($e) => str_contains($e, "input")); // true $isTimeout = $error->isErrAnd(fn($e) => str_contains($e, "timeout")); // false
Transformation Methods
map
Transform the success value while preserving the error:
$result = new Ok(5); $doubled = $result->map(fn($x) => $x * 2); // Ok(10) $error = new Err("failed"); $doubled = $error->map(fn($x) => $x * 2); // Err("failed")
mapErr
Transform the error value while preserving the success:
$result = new Err("error"); $mapped = $result->mapErr(fn($e) => "Error: " . $e); // Err("Error: error") $success = new Ok(42); $mapped = $success->mapErr(fn($e) => "Error: " . $e); // Ok(42)
andThen
Chain operations that might fail:
function divide($x, $y): Result { return $y === 0 ? new Err("division by zero") : new Ok($x / $y); } $result = new Ok(10) ->andThen(fn($x) => divide($x, 2)) // Ok(5) ->andThen(fn($x) => divide($x, 0)); // Err("division by zero")
Matching
match
Handle both success and error cases:
$result = new Ok(42); $value = $result->match( fn($value) => "Success: $value", fn($error) => "Error: $error" ); // "Success: 42"
Inspection Methods
inspect
Perform side effects on success value without consuming the Result:
$result = new Ok(42); $result->inspect(fn($value) => print("Got value: $value")) ->map(fn($x) => $x * 2);
inspectErr
Perform side effects on error value without consuming the Result:
$error = new Err("error"); $error->inspectErr(fn($err) => print("Error occurred: $err")) ->mapErr(fn($err) => "Handled: $err");
Unwrapping Methods
unwrap
Get the success value or throw an exception:
$result = new Ok(42); $value = $result->unwrap(); // 42 $error = new Err("oops"); $value = $error->unwrap(); // throws UnwrapException
unwrapErr
Get the error value or throw an exception:
$error = new Err("error message"); $value = $error->unwrapErr(); // "error message" $success = new Ok(42); $value = $success->unwrapErr(); // throws UnwrapException
unwrapOr
Get the value or a default:
$result = new Err("error"); $value = $result->unwrapOr(42); // 42
unwrapOrElse
Get the value or compute a default:
$result = new Err("error"); $value = $result->unwrapOrElse(fn($error) => strlen($error)); // 5
unwrapOrDefault
Get the success value or the type's default value:
class DefaultProvider implements DefaultValueProvider { public function getDefault(): int { return 0; } } $result = new Err("error"); $value = $result->unwrapOrDefault(new DefaultProvider()); // 0
expect
Unwrap with a custom error message:
$result = new Ok(42); $value = $result->expect("This should never fail"); // 42 $error = new Err("oops"); $value = $error->expect("Critical error"); // throws UnwrapException with message "Critical error"
expectErr
Get the error value with a custom error message:
$error = new Err("not found"); $value = $error->expectErr("Expected error not found"); // "not found" $success = new Ok(42); $value = $success->expectErr("Expected an error"); // throws UnwrapException with message "Expected an error"
unwrapUnchecked
Get the success value without safety checks (use with caution):
// WARNING: Only use when you are absolutely certain the Result is Ok // Otherwise, it will cause undefined behavior $result = new Ok(42); $value = $result->unwrapUnchecked(); // 42
unwrapErrUnchecked
Get the error value without safety checks (use with caution):
// WARNING: Only use when you are absolutely certain the Result is Err // Otherwise, it will cause undefined behavior $error = new Err("error"); $value = $error->unwrapErrUnchecked(); // "error"
Real-world Examples
Example 1: User Authentication
class UserAuthenticator { public function authenticate(string $username, string $password): Result { $user = $this->findUser($username); if ($user === null) { return new Err("User not found"); } return password_verify($password, $user->password) ? new Ok($user) : new Err("Invalid password"); } } // Usage $auth = new UserAuthenticator(); $result = $auth->authenticate("john", "password123") ->map(fn($user) => $user->toArray()) ->mapErr(fn($error) => ["error" => $error]);
Example 2: API Response Handling
class ApiClient { public function fetchData(string $url): Result { try { $response = file_get_contents($url); $data = json_decode($response, true); return $data === null ? new Err("Invalid JSON response") : new Ok($data); } catch (Throwable $e) { return new Err($e->getMessage()); } } } // Usage $client = new ApiClient(); $result = $client->fetchData("https://api.example.com/data") ->andThen(function($data) { return isset($data['items']) ? new Ok($data['items']) : new Err("Missing items in response"); }) ->map(fn($items) => array_map(fn($item) => $item['name'], $items));
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. Make sure to read the contributing guidelines before submitting your PR.
License
This project is licensed under the MIT License - see the LICENSE file for details.