PHPackages                             yannelli/attempt - 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. [Debugging &amp; Profiling](/categories/debugging)
4. /
5. yannelli/attempt

ActiveLibrary[Debugging &amp; Profiling](/categories/debugging)

yannelli/attempt
================

A fluent attempt/retry/fallback system for Laravel

v1.0.0(2mo ago)7512—10%MITPHPPHP ^8.4CI passing

Since Jan 19Pushed 2mo agoCompare

[ Source](https://github.com/yannelli/attempt)[ Packagist](https://packagist.org/packages/yannelli/attempt)[ Docs](https://github.com/yannelli/attempt)[ GitHub Sponsors](https://github.com/yannelli)[ RSS](/packages/yannelli-attempt/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (3)Dependencies (22)Versions (4)Used By (0)

Attempt
=======

[](#attempt)

[![Latest Version on Packagist](https://camo.githubusercontent.com/7480c4124593b31a50e6527310ef6f8e0f2f632b87ce35d9b148cafd12806a33/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f79616e6e656c6c692f617474656d70742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/yannelli/attempt)[![GitHub Tests Action Status](https://camo.githubusercontent.com/0075b3714786bdc17cbe5fae948f9020fe9434a13b076824dd53d9badca2c79c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f79616e6e656c6c692f617474656d70742f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/yannelli/attempt/actions?query=workflow%3Arun-tests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/4675c8b9caaf3d162d9d5627c18de1110b1fd90fb36d3fa2ece967bed12843a4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f79616e6e656c6c692f617474656d70742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/yannelli/attempt)

- [Introduction](#introduction)
- [Installation](#installation)
- [Making Attempts](#making-attempts)
    - [Basic Usage](#basic-usage)
    - [Attemptable Classes](#attemptable-classes)
    - [Self-Configuring Classes](#self-configuring-classes)
- [Retry Configuration](#retry-configuration)
    - [Specifying Retry Attempts](#specifying-retry-attempts)
    - [Delay Strategies](#delay-strategies)
    - [Conditional Retries](#conditional-retries)
- [Fallback Handlers](#fallback-handlers)
    - [Defining Fallbacks](#defining-fallbacks)
    - [Fallback Chains](#fallback-chains)
    - [The Fallbackable Interface](#the-fallbackable-interface)
- [Exception Handling](#exception-handling)
    - [Catching Exceptions](#catching-exceptions)
    - [Re-throwing Exceptions](#re-throwing-exceptions)
    - [Suppressing Exceptions](#suppressing-exceptions)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Conditional Execution](#conditional-execution)
- [Pipeline Integration](#pipeline-integration)
    - [Pipeline Attempts](#pipeline-attempts)
    - [Using AttemptPipe](#using-attemptpipe)
- [Concurrent Execution](#concurrent-execution)
    - [Running Concurrent Attempts](#running-concurrent-attempts)
    - [Racing Attempts](#racing-attempts)
- [Async Execution](#async-execution)
- [Working with Results](#working-with-results)
    - [The AttemptResult Object](#the-attemptresult-object)
    - [Monadic Operations](#monadic-operations)
- [Events](#events)
- [Testing](#testing)
- [Configuration](#configuration)

Introduction
------------

[](#introduction)

While building your application, you may encounter operations that can fail due to transient issues like network timeouts, API rate limits, or temporary service unavailability. Rather than letting these failures crash your application or writing repetitive try-catch blocks, Laravel Attempt provides a fluent, composable system for handling retries, fallbacks, and error recovery.

Attempt treats error handling as a first-class pipeline concern, allowing you to declaratively define how your application should respond when things go wrong. Whether you need simple retry logic with exponential backoff, complex fallback chains, or integration with Laravel’s native Pipeline, Attempt provides an expressive API that reads like natural language.

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

[](#installation)

You may install Attempt into your project using the Composer package manager:

```
composer require yannelli/attempt
```

After installing Attempt, you may optionally publish its configuration file using the `vendor:publish` Artisan command:

```
php artisan vendor:publish --tag="attempt-config"
```

Making Attempts
---------------

[](#making-attempts)

### Basic Usage

[](#basic-usage)

The simplest way to use Attempt is to wrap a potentially failing operation with the `try` method. To execute the attempt and retrieve the result, you may call the `thenReturn` method:

```
use Yannelli\Attempt\Facades\Attempt;

$result = Attempt::try(fn() => $api->call())->thenReturn();
```

If you need to pass input to your callable, you may provide additional arguments to the `try` method:

```
$result = Attempt::try(MyAction::class, $order, $user)->thenReturn();
```

You may also pass an array of callables to the `try` method. When an array is provided, each callable will be executed in order as a fallback chain. If the first callable fails, the second will be attempted, and so on:

```
$result = Attempt::try([
    PrimaryProvider::class,
    BackupProvider::class,
    CachedResponse::class,
], $payload)->thenReturn();
```

Attempt provides several methods for executing your attempt and retrieving the result:

MethodBehavior`then(Closure $callback)`Transform and return the final result`thenReturn()`Return the processed value directly`thenReturnOrFail()`Return the value or throw on failure`run()`Return an `AttemptResult` object with metadata`get()`Alias for `thenReturn()``value()`Alias for `thenReturn()`### Attemptable Classes

[](#attemptable-classes)

For more complex operations, you may create dedicated attemptable classes. These classes should implement the `Attemptable` interface and define a `handle` method that receives the input and returns a result:

```
use Yannelli\Attempt\Contracts\Attemptable;

class FetchUserData implements Attemptable
{
    public function handle(mixed ...$input): mixed
    {
        [$userId] = $input;

        return Http::get("https://api.example.com/users/{$userId}")->json();
    }
}
```

Once you have defined your attemptable class, you may pass its class name to the `try` method:

```
$userData = Attempt::try(FetchUserData::class, $userId)
    ->retry(3)
    ->thenReturn();
```

### Self-Configuring Classes

[](#self-configuring-classes)

Sometimes you may want a class to define its own retry and fallback configuration. To accomplish this, your class may implement both the `Attemptable` and `ConfiguresAttempt` interfaces. The `configureAttempt` method receives an `AttemptBuilder` instance that you may use to define your preferred configuration:

```
use Yannelli\Attempt\Contracts\Attemptable;
use Yannelli\Attempt\Contracts\ConfiguresAttempt;
use Yannelli\Attempt\AttemptBuilder;

class ResilientApiCall implements Attemptable, ConfiguresAttempt
{
    public function configureAttempt(AttemptBuilder $attempt): void
    {
        $attempt
            ->retry(3)
            ->exponentialBackoff(100, 5000)
            ->withJitter(0.1);
    }

    public function handle(mixed ...$input): mixed
    {
        return Http::get('https://api.example.com/data')->json();
    }
}
```

When using a self-configuring class, the configuration is automatically applied:

```
$result = Attempt::try(ResilientApiCall::class)->thenReturn();
```

Retry Configuration
-------------------

[](#retry-configuration)

### Specifying Retry Attempts

[](#specifying-retry-attempts)

By default, Attempt will not retry a failed operation. To enable retries, call the `retry` method and specify how many times the operation should be attempted:

```
Attempt::try($callable)
    ->retry(3)
    ->thenReturn();
```

### Delay Strategies

[](#delay-strategies)

Often, you will want to wait between retry attempts to give transient issues time to resolve. Attempt provides several strategies for configuring delays between retries.

#### Fixed Delay

[](#fixed-delay)

To wait a fixed number of milliseconds between all retries, pass an integer to the `delay` method:

```
Attempt::try($callable)
    ->retry(3)
    ->delay(100) // Wait 100ms between retries
    ->thenReturn();
```

#### Explicit Delays

[](#explicit-delays)

If you need different delays for each retry attempt, you may pass an array of millisecond values:

```
Attempt::try($callable)
    ->retry(3)
    ->delay([1000, 5000, 15000]) // 1s, 5s, 15s
    ->thenReturn();
```

#### Exponential Backoff

[](#exponential-backoff)

Exponential backoff progressively increases the delay between retries. This strategy is particularly useful when interacting with rate-limited APIs or overloaded services. The `exponentialBackoff` method accepts a base delay and an optional maximum delay:

```
Attempt::try($callable)
    ->retry(5)
    ->exponentialBackoff(base: 100, max: 30000) // 100ms, 200ms, 400ms, 800ms...
    ->thenReturn();
```

#### Linear Backoff

[](#linear-backoff)

Linear backoff increases the delay by a fixed increment with each retry:

```
Attempt::try($callable)
    ->retry(3)
    ->linearBackoff(base: 100, increment: 100) // 100ms, 200ms, 300ms
    ->thenReturn();
```

#### Adding Jitter

[](#adding-jitter)

To prevent multiple failing operations from retrying in lockstep (known as the “thundering herd” problem), you may add randomized jitter to your delays. The `withJitter` method accepts a percentage value that determines how much variance to apply:

```
Attempt::try($callable)
    ->retry(3)
    ->delay([1000, 5000, 10000])
    ->withJitter(0.2) // +/- 20% randomization
    ->thenReturn();
```

#### Custom Delay Functions

[](#custom-delay-functions)

For complete control over delay calculation, you may use the `delayUsing` method with a closure that receives the current attempt number and the exception that triggered the retry:

```
Attempt::try($callable)
    ->retry(5)
    ->delayUsing(fn(int $attempt, ?Throwable $e) => $attempt * 1000)
    ->thenReturn();
```

### Conditional Retries

[](#conditional-retries)

Sometimes you may only want to retry an operation for specific types of failures. The `retryIf` method accepts a closure that receives the thrown exception and returns a boolean indicating whether the operation should be retried:

```
Attempt::try($callable)
    ->retry(3)
    ->retryIf(fn(Throwable $e) => $e instanceof ConnectionException)
    ->thenReturn();
```

Fallback Handlers
-----------------

[](#fallback-handlers)

### Defining Fallbacks

[](#defining-fallbacks)

When an operation fails after exhausting all retries, you may want to execute a fallback operation instead of throwing an exception. Use the `fallback` method to define an alternative callable:

```
Attempt::try(PrimaryApi::class)
    ->fallback(BackupApi::class)
    ->thenReturn();
```

### Fallback Chains

[](#fallback-chains)

You may define multiple fallbacks that will be tried in order. The first successful fallback wins:

```
Attempt::try(PrimaryApi::class)
    ->fallback([
        SecondaryApi::class,
        TertiaryApi::class,
        fn() => Cache::get('fallback_value'),
    ])
    ->thenReturn();
```

For a more expressive syntax, you may chain multiple `orFallback` calls:

```
Attempt::try(PrimaryApi::class)
    ->orFallback(SecondaryApi::class)
    ->orFallback(fn() => 'default')
    ->thenReturn();
```

### The Fallbackable Interface

[](#the-fallbackable-interface)

For fallback classes that need access to the original exception, implement the `Fallbackable` interface. This interface defines a `handleFallback` method that receives both the exception and the original input:

```
use Yannelli\Attempt\Contracts\Fallbackable;

class ApiErrorFallback implements Fallbackable
{
    public function handleFallback(Throwable $e, mixed ...$input): mixed
    {
        Log::warning('Using fallback due to: ' . $e->getMessage());

        return Cache::get('cached_response');
    }

    public function shouldSkip(Throwable $e): bool
    {
        // Skip this fallback for certain exceptions
        return $e instanceof ValidationException;
    }
}
```

Exception Handling
------------------

[](#exception-handling)

### Catching Exceptions

[](#catching-exceptions)

Attempt allows you to register exception handlers that will be invoked when specific exceptions occur. You may catch specific exception types or all exceptions:

```
// Catch specific exceptions
Attempt::try($callable)
    ->catch(ConnectionException::class, fn($e) => Log::error($e))
    ->catch(TimeoutException::class, fn($e) => Metrics::timeout())
    ->thenReturn();

// Catch all exceptions
Attempt::try($callable)
    ->catch(fn(Throwable $e) => Log::error($e))
    ->thenReturn();
```

### Re-throwing Exceptions

[](#re-throwing-exceptions)

If you want to execute a handler but still throw the exception afterward, chain the `throw` method:

```
Attempt::try($callable)
    ->catch(fn($e) => Log::error($e))
    ->throw()
    ->thenReturn();
```

### Suppressing Exceptions

[](#suppressing-exceptions)

To suppress all exceptions and return `null` on failure, use the `quiet` method:

```
Attempt::try($callable)
    ->quiet()
    ->thenReturn(); // Returns null on failure
```

Lifecycle Hooks
---------------

[](#lifecycle-hooks)

Attempt provides several hooks that allow you to execute code at specific points during the attempt lifecycle:

```
Attempt::try($callable)
    ->finally(fn($context) => Log::info('Attempt completed'))
    ->defer(fn($context) => Metrics::record($context->elapsed()))
    ->onRetry(fn($context, $e) => Log::warning("Retry {$context->attemptNumber}"))
    ->onSuccess(fn($context, $result) => Cache::put('last_result', $result))
    ->onFailure(fn($context, $e) => Alert::send($e))
    ->thenReturn();
```

Conditional Execution
---------------------

[](#conditional-execution)

You may conditionally execute an attempt using the `when` and `unless` methods:

```
// Only execute if condition is true
Attempt::try($callable)
    ->when($shouldRun)
    ->thenReturn();

// Only execute if condition is false
Attempt::try($callable)
    ->unless($shouldSkip)
    ->thenReturn();

// With closure conditions
Attempt::try($callable)
    ->when(fn() => Feature::active('new-api'))
    ->thenReturn();
```

Pipeline Integration
--------------------

[](#pipeline-integration)

### Pipeline Attempts

[](#pipeline-attempts)

Attempt integrates seamlessly with Laravel’s Pipeline. Use the `pipeline` method to execute a series of stages with built-in retry and fallback capabilities:

```
$result = Attempt::pipeline([
    ValidateInput::class,
    ProcessData::class,
    SaveToDatabase::class,
])
    ->send($data)
    ->retry(2)
    ->thenReturn();
```

### Using AttemptPipe

[](#using-attemptpipe)

You may also use `AttemptPipe` within a native Laravel Pipeline to wrap individual stages with retry logic:

```
use Illuminate\Support\Facades\Pipeline;
use Yannelli\Attempt\Pipes\AttemptPipe;

$result = Pipeline::send($data)
    ->through([
        AttemptPipe::wrap(ExternalApiCall::class)
            ->retry(3)
            ->delay([100, 500, 1000]),
        ProcessResponse::class,
    ])
    ->thenReturn();
```

Concurrent Execution
--------------------

[](#concurrent-execution)

### Running Concurrent Attempts

[](#running-concurrent-attempts)

When you need to execute multiple operations simultaneously, use the `concurrent` method. All operations will run in parallel, and you will receive an array of results:

```
$concurrent = Attempt::concurrent([
    fn() => Http::get('https://api1.example.com'),
    fn() => Http::get('https://api2.example.com'),
    fn() => Http::get('https://api3.example.com'),
]);

// Run all and get array of AttemptResult objects
$results = $concurrent->run();

// Get only successful results
$successful = Attempt::concurrent([...])->successful();

// Get only failed results
$failed = Attempt::concurrent([...])->failed();

// Get values directly
$values = Attempt::concurrent([...])->thenReturn();
```

### Racing Attempts

[](#racing-attempts)

When you need the result of the first successful operation, use the `race` method. The first operation to succeed wins, and other operations are abandoned:

```
$result = Attempt::race([
    PrimaryProvider::class,
    SecondaryProvider::class,
    TertiaryProvider::class,
])->thenReturn();
```

Async Execution
---------------

[](#async-execution)

For long-running operations, you may dispatch an attempt to run asynchronously on the queue:

```
Attempt::try(LongRunningTask::class, $data)
    ->retry(3)
    ->async()
    ->onQueue('processing')
    ->then(fn ($value) => Log::info('Completed', ['value' => $value]))
    ->catch(fn (Throwable $e) => Log::error('Failed', ['error' => $e->getMessage()]))
    ->dispatch();
```

If you need to execute the attempt synchronously instead (bypassing the queue), you may use the `await` method:

```
$result = Attempt::try(LongRunningTask::class, $data)
    ->retry(3)
    ->async()
    ->await(); // Runs synchronously, returns AttemptResult
```

Working with Results
--------------------

[](#working-with-results)

### The AttemptResult Object

[](#the-attemptresult-object)

When you call the `run` method instead of `thenReturn`, you receive an `AttemptResult` object that provides detailed information about the attempt:

```
$result = Attempt::try($callable)->run();

// Check status
$result->succeeded();  // bool
$result->failed();     // bool

// Get values
$result->value();      // mixed - the result value
$result->exception();  // ?Throwable - the exception if failed
$result->attempts();   // int - number of attempts made
$result->resolvedBy(); // string - 'primary', 'retry:2', 'fallback:ClassName'
```

### Monadic Operations

[](#monadic-operations)

The `AttemptResult` object supports monadic operations for functional-style programming:

```
$result->map(fn($value) => transform($value));
$result->getOrElse('default');
$result->getOrThrow();
$result->onSuccess(fn($value) => doSomething($value));
$result->onFailure(fn($e) => handleError($e));
```

Events
------

[](#events)

Attempt dispatches events throughout the attempt lifecycle, allowing you to hook into various stages for logging, monitoring, or other purposes:

EventWhen Fired`AttemptStarted`When the attempt begins`AttemptSucceeded`On successful completion`AttemptFailed`On each failure (before retry)`RetryAttempted`When a retry is initiated`FallbackTriggered`When a fallback is tried`AllAttemptsFailed`When all attempts and fallbacks failIf you need to disable events for a specific attempt, use the `withoutEvents` method:

```
Attempt::try($callable)
    ->withoutEvents()
    ->thenReturn();
```

Testing
-------

[](#testing)

Attempt includes a convenient fake implementation for testing. Use the `fake` method to replace the Attempt facade with a test double:

```
use Yannelli\Attempt\Facades\Attempt;

it('retries on failure', function () {
    Attempt::fake()->sequence([
        new ConnectionException('Failed'),
        new ConnectionException('Failed'),
        ['success' => true],
    ]);

    $result = Attempt::try(MyApiCall::class)
        ->retry(3)
        ->run();

    expect($result->succeeded())->toBeTrue();
    expect($result->attempts())->toBe(3);

    Attempt::assertAttemptedTimes(MyApiCall::class, 3);
});

it('uses fallback when all retries fail', function () {
    Attempt::fake()->failFor(PrimaryApi::class, times: 5);

    $result = Attempt::try(PrimaryApi::class)
        ->retry(3)
        ->fallback(BackupApi::class)
        ->run();

    Attempt::assertFallbackUsed(BackupApi::class);
});
```

To run the package’s test suite:

```
composer test
```

Configuration
-------------

[](#configuration)

The published configuration file (`config/attempt.php`) allows you to customize default behaviors:

```
return [
    'defaults' => [
        'max_retries' => 3,
        'delay' => 100,
        'backoff' => 'exponential',
        'jitter' => 0.1,
    ],

    'backoff_strategies' => [
        'exponential' => [...],
        'linear' => [...],
        'fibonacci' => [...],
        'decorrelated_jitter' => [...],
    ],

    'async' => [
        'connection' => env('ATTEMPT_QUEUE_CONNECTION'),
        'queue' => env('ATTEMPT_QUEUE'),
        'timeout' => 60,
    ],

    'events' => [
        'enabled' => true,
    ],

    // Exceptions that should never trigger retries
    'never_retry' => [
        ValidationException::class,
        AuthenticationException::class,
        AuthorizationException::class,
        ModelNotFoundException::class,
    ],

    // Exceptions that should always trigger retries
    'always_retry' => [
        ConnectionException::class,
    ],
];
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Ryan Yannelli](https://ryanyannelli.com)
- [Nextvisit AI](https://nextvisit.ai)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

46

—

FairBetter than 93% of packages

Maintenance87

Actively maintained with recent releases

Popularity23

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 97% 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

Every ~27 days

Total

3

Last Release

66d ago

Major Versions

v0.1.1 → v1.0.02026-03-14

PHP version history (2 changes)v0.1.0PHP ^8.2

v1.0.0PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/53b64331d4de8c9cef47bc20707ed728c0e900cd2bda4ed7d95359ced8b9adf1?d=identicon)[yannelli](/maintainers/yannelli)

---

Top Contributors

[![yannelli](https://avatars.githubusercontent.com/u/59575788?v=4)](https://github.com/yannelli "yannelli (32 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

backoffcomposercomposer-packageerror-handlingexception-handlingexponential-backofffallbackfault-tolerancelaravellaravel-packagephppipelineresilienceretryretry-strategylaravelretryerror handlingfallbackattemptyannelli

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/yannelli-attempt/health.svg)

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

###  Alternatives

[laravel/horizon

Dashboard and code-driven configuration for Laravel queues.

4.1k84.2M225](/packages/laravel-horizon)[spatie/laravel-ray

Easily debug Laravel apps

31738.4M2.8k](/packages/spatie-laravel-ray)[laragear/preload

Effortlessly make a Preload script for your Laravel application.

119363.5k](/packages/laragear-preload)[clickbar/laravel-magellan

This package provides functionality for working with the postgis extension in Laravel.

423715.4k1](/packages/clickbar-laravel-magellan)[spatie/laravel-prometheus

Export Laravel metrics to Prometheus

2651.3M6](/packages/spatie-laravel-prometheus)[stephenjude/filament-debugger

About

102125.4k2](/packages/stephenjude-filament-debugger)

PHPackages © 2026

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