PHP code example of hiblaphp / promise

1. Go to this page and download the library: Download hiblaphp/promise library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

hiblaphp / promise example snippets


use Hibla\Promise\Promise;
use function Hibla\delay;

// Fetch two promises concurrently and combine their results
Promise::all([
    'user'   => fetchUser(1),
    'orders' => fetchOrders(1),
])
->then(function (array $results) {
    echo "User: {$results['user']->name}\n";
    echo "Orders: " . count($results['orders']) . "\n";
})
->catch(fn(\Throwable $e) => error_log($e->getMessage()));

// Pause until a promise settles — useful at the top level of a script
$user = fetchUser(1)->wait(); // returns the resolved value, or throws on rejection

// Wrap any async work in a cancellable promise
$job = new Promise(function ($resolve, $reject, $onCancel) {
    $timerId = Loop::addTimer(10.0, fn() => $resolve('done'));
    $onCancel(fn() => Loop::cancelTimer($timerId)); // cleans up if cancelled
});

// Cancel it after 2 seconds — the timer is cleaned up immediately
delay(2.0)->then(fn() => $job->cancel());

// Callback hell — error handling is duplicated, nesting grows with every step
fetchUser(1, function ($user, $error) {
    if ($error) {
        logError($error);
        return;
    }
    fetchOrders($user->id, function ($orders, $error) {
        if ($error) {
            logError($error);
            return;
        }
        fetchInvoices($orders, function ($invoices, $error) {
            if ($error) {
                logError($error);
                return;
            }
            // now you can finally do something useful
        });
    });
});

fetchUser(1)
    ->then(fn($user)     => fetchOrders($user->id))
    ->then(fn($orders)   => fetchInvoices($orders))
    ->then(fn($invoices) => processInvoices($invoices))
    ->catch(fn($e)       => logError($e)); // one handler covers the entire chain

// Standard promise implementations — cancellation is modelled as rejection.
// Cleanup and error handling are mixed together in the same catch() handler.
// Every catch site has to inspect the exception type to separate the two.
$promise->cancel();
$promise->catch(function (\Throwable $e) {
    if ($e instanceof CancellationException) {
        // cleanup path — but this is inside catch(), which is meant for errors
    } else {
        // error path — but now every error handler carries cancellation boilerplate
    }
});

// Hibla — cleanup and error handling are completely separate concerns
$promise->onCancel(function () {
    // cleanup path — runs only on deliberate cancellation
    // never triggered by a real failure
    closeConnection();
    releaseResources();
});

$promise->catch(function (\Throwable $e) {
    // error path — runs only on genuine failures
    // never triggered by a cancellation
    logError($e);
    notifyUser($e);
});

$promise->cancel();

$promise->isCancelled(); // true  — clean, unambiguous
$promise->isRejected();  // false — nothing went wrong
$promise->isFulfilled(); // false — no result was produced

// Cancelling an already-fulfilled promise — no-op
$promise = Promise::resolved('done');
$promise->cancel();
$promise->isCancelled(); // false
$promise->isFulfilled(); // true — result is unchanged

// Cancelling an already-rejected promise — no-op
$promise = Promise::rejected(new \RuntimeException('Failed'));
$promise->cancel();
$promise->isCancelled(); // false
$promise->isRejected();  // true — rejection reason is unchanged

// Cancelling a pending promise — works as expected
$promise = new Promise();
$promise->cancel();
$promise->isCancelled(); // true

// Race condition — resolve wins if it arrives before cancel()
$promise = new Promise();
$promise->resolve('done');
$promise->cancel();       // no-op — already fulfilled
$promise->isCancelled();  // false
$promise->isFulfilled();  // true

Promise::all([
    fetchUser(1),    // rejects with DatabaseException
    fetchOrders(1),  // cancelled as side effect — onCancel() fires, no error reported
    fetchStats(1),   // cancelled as side effect — onCancel() fires, no error reported
])->catch(function (\Throwable $e) {
    // $e is DatabaseException — exactly one error, from exactly one source
    // The two cancellations are invisible here because they were not failures
    echo $e->getMessage();
});

use Hibla\Promise\Promise;

$promise = new Promise(function (callable $resolve, callable $reject) {
    $resolve('Hello, World!');
});

$promise->then(function (string $value) {
    echo $value; // Hello, World!
});

$promise = new Promise(function (callable $resolve, callable $reject) {
    throw new \RuntimeException('Something went wrong');
});

$promise->catch(function (\Throwable $e) {
    echo $e->getMessage(); // Something went wrong
});

$promise = new Promise(function (callable $resolve, callable $reject, callable $onCancel) {
    $timerId = Loop::addTimer(5, fn() => $resolve('done'));

    // Cleanup lives right next to the work — harder to forget
    $onCancel(fn() => Loop::cancelTimer($timerId));
});

$promise = new Promise(function (callable $resolve, callable $reject, callable $onCancel) {
    $timerId = Loop::addTimer(5, fn() => $resolve('done'));
    $handle  = fopen('data.tmp', 'w');

    $onCancel(fn() => Loop::cancelTimer($timerId)); // runs first
    $onCancel(fn() => fclose($handle));             // runs second
});

$promise = new Promise(function (callable $resolve, callable $reject, callable $onCancel) {
    $onCancel(fn() => /* runs 1st */);
    $onCancel(fn() => /* runs 2nd */);
});

$promise->onCancel(fn() => /* runs 3rd */);
$promise->onCancel(fn() => /* runs 4th */);

$promise = new Promise();

Loop::addTimer(1.0, function () use ($promise) {
    $promise->resolve('Done after 1 second');
});

$promise->then(fn($value) => print($value));

// Already fulfilled
$promise = Promise::resolved('immediate value');

// Already rejected
$promise = Promise::rejected(new \RuntimeException('Already failed'));

$promise = new Promise(); // No executor — starts pending

Loop::addTimer(1.0, function () use ($promise) {
    $promise->resolve('Done after 1 second');
});

$promise->then(fn($value) => print($value));

// Constructor pattern — work and cleanup live in the same place
$promise = new Promise(function ($resolve, $reject, $onCancel) {
    $timerId = Loop::addTimer(5, fn() => $resolve('done'));
    $onCancel(fn() => Loop::cancelTimer($timerId));
});

$promise = new Promise();

$machine->onTransition(function (State $from, State $to) use ($promise) {
    if ($to === State::COMPLETED) $promise->resolve($machine->result());
    if ($to === State::FAILED)    $promise->reject($machine->error());
    if ($to === State::ABORTED)   $promise->reject(new AbortedException());
});

$promise->onCancel(fn() => $machine->forceStop());

$promise = new Promise();

$socket->onConnect(fn()  => $promise->resolve($socket));
$socket->onError(fn($e)  => $promise->reject($e));
$socket->onTimeout(fn()  => $promise->reject(new TimeoutException()));
$promise->onCancel(fn()  => $socket->close());

$promise = new Promise();
$barrier = new CountdownLatch(3);

$bus->on('worker.done', function () use ($barrier, $promise) {
    $barrier->countDown();
    if ($barrier->isDone()) {
        $promise->resolve();
    }
});

$promise = new Promise(function ($resolve, $reject, $onCancel) {
    $timerId = Loop::addTimer(5.0, fn() => $resolve('Finished'));
    $onCancel(fn() => Loop::cancelTimer($timerId));
});

$promise = new Promise();

$timerId = Loop::addTimer(5.0, function () use ($promise) {
    $promise->resolve('Finished');
});

// Without this, cancelling $promise leaves the timer alive for 5 seconds
$promise->onCancel(function () use ($timerId) {
    Loop::cancelTimer($timerId);
});

function delay(float $seconds): PromiseInterface
{
    $promise = new Promise();

    $timerId = Loop::addTimer($seconds, function () use ($promise): void {
        $promise->resolve(null);
    });

    $promise->onCancel(function () use ($timerId): void {
        Loop::cancelTimer($timerId);
    });

    return $promise;
}

// Wrapping a stream read into a promise
function readOnce($stream): PromiseInterface
{
    $promise = new Promise();

    $watcherId = Loop::addReadWatcher($stream, function ($stream) use ($promise, &$watcherId) {
        Loop::removeReadWatcher($watcherId);
        $promise->resolve(fread($stream, 4096));
    });

    $promise->onCancel(function () use (&$watcherId) {
        Loop::removeReadWatcher($watcherId);
    });

    return $promise;
}

Promise::resolved(1)
    ->then(fn($n) => $n + 1)      // 2
    ->then(fn($n) => $n * 10)     // 20
    ->then(fn($n) => print($n));  // prints 20

$promise = Promise::resolved('immediate');

$promise->then(fn($v) => print("B: $v\n")); // registered — but not called yet

print("A\n"); // runs first — we are still in synchronous code

// Output:
// A
// B: immediate   ← then() fires only after sync code finishes

$promise = new Promise();

$promise->then(fn($v) => print("C: $v\n"));

print("A\n");
$promise->resolve('hello'); // resolve() called — but then() not invoked yet
print("B\n");               // still runs before the then() handler

// Output:
// A
// B
// C: hello

$promise = Promise::resolved(0);

for ($i = 0; $i < 10000; $i++) {
    $promise = $promise->then(fn($n) => $n + 1);
}

$promise->then(fn($n) => print("Final: $n\n")); // Final: 10000
// Stack depth at every handler: flat — always the same depth regardless
// of chain length

// Recovery — catch() returns a value, chain continues fulfilled
Promise::rejected(new \RuntimeException('Oops'))
    ->catch(fn($e) => 'recovered')
    ->then(fn($v)  => print("fulfilled: $v\n")); // fulfilled: recovered

// Re-throw — catch() throws, chain stays rejected
Promise::rejected(new \RuntimeException('Oops'))
    ->catch(function (\Throwable $e) {
        logger()->error($e->getMessage());
        throw $e; // re-throw — next catch() in the chain receives it
    })
    ->catch(fn($e) => print("still rejected: {$e->getMessage()}\n"));

Promise::resolved('ok')
    ->catch(fn($e) => 'this never runs') // nothing to catch — promise above fulfilled
    ->then(fn($v)  => throw new \RuntimeException('thrown in then()'))
    ->catch(fn($e) => print("caught: {$e->getMessage()}\n")); // caught: thrown in then()

fetchUser(1)
    ->then(fn($user)     => fetchOrders($user->id))
    ->then(fn($orders)   => fetchInvoices($orders))
    ->then(fn($invoices) => processInvoices($invoices))
    ->catch(fn($e)       => logError($e)); // covers any failure in the chain above

$promise
    ->then(fn($v)  => processResult($v))
    ->catch(fn($e) => logError($e))      // never called on cancellation
    ->finally(fn() => closeConnection()); // always called — including cancellation

use React\Promise\Deferred;

$promise
    ->then(function ($value) {
        $deferred = new Deferred();

        Loop::addTimer(1.0, fn() => $deferred->resolve($value . ' from react'));

        // Return a ReactPHP promise — Hibla detects the then() method
        // and waits for it to settle before continuing the chain
        return $deferred->promise();
    })
    ->then(function ($result) {
        echo "Got: $result\n"; // Got: hello from react
    });

$outer = fetchUser(1)->then(fn($user) => fetchOrders($user->id));

// fetchOrders returns a Hibla PromiseInterface — cancellation propagates
$outer->cancel();
// fetchOrders promise is also cancelled, its onCancel() handlers run

// Hibla PromiseInterface — cancellation propagates ✓
$outer = $promise->then(fn($v) => hiblaDerivedPromise($v));
$outer->cancel(); // inner promise also cancelled

// Foreign thenable — cancellation does NOT propagate ✗
$outer = $promise->then(fn($v) => $reactPhpPromise);
$outer->cancel(); // outer state changes, but $reactPhpPromise keeps running

$outer = $promise->then(function ($v) use (&$foreignPromise) {
    $foreignPromise = fetchWithReactPhp($v);
    return $foreignPromise;
});

$outer->onCancel(function () use (&$foreignPromise) {
    if ($foreignPromise !== null) {
        $foreignPromise->cancel();
    }
});

$promise = new Promise();

$chained = $promise->then(function () use (&$chained) {
    return $chained; // returns itself — cyclic chain
});

$promise->resolve('value');

$chained->catch(function (\TypeError $e) {
    echo $e->getMessage(); // "Chaining cycle detected"
});

$promise
    ->then(fn($data) => processData($data))
    ->catch(fn($e)   => logError($e))
    ->finally(fn()   => closeConnection());

Loop::addPeriodicTimer(1.0, fn() => print("tick\n"), maxExecutions: 5);

delay(5)->then(fn() => print("5 seconds passed\n"))->wait();

echo "Done\n";

// Output:
// tick          ← periodic timer fires normally during wait()
// tick
// tick
// tick
// tick
// 5 seconds passed
// Done          ← script advances only after wait() returns

try {
    Promise::rejected(new \RuntimeException('Failed'))->wait();
} catch (\RuntimeException $e) {
    echo $e->getMessage(); // Failed
}

try {
    Promise::rejected('quota exceeded')->wait();
} catch (\Hibla\Promise\Exceptions\PromiseRejectionException $e) {
    echo $e->getMessage(); // quota exceeded
}

try {
    $promise = new Promise();
    Loop::addTimer(1.0, fn() => $promise->cancel());
    $promise->wait(); // promise is cancelled during the loop iterations
} catch (CancelledException $e) {
    echo $e->getMessage(); // Promise was cancelled during wait
}

use function Hibla\delay;

delay(1.5)->then(fn() => print("1.5 seconds later\n"));

$promise = new Promise(function ($resolve, $reject, $onCancel) {
    $timerId = Loop::addTimer(10.0, fn() => $resolve('done'));
    $onCancel(fn() => Loop::cancelTimer($timerId));
});

$promise->cancel();

var_dump($promise->isCancelled()); // true
var_dump($promise->isRejected());  // false

$promise = new Promise();
$promise->onCancel(function () {
    echo "A\n"; // prints first
});
$promise->onCancel(function () {
    echo "B\n"; // prints second
});

$promise->cancel();
echo "C\n"; // prints third — after both handlers have already run

// Via executor argument — co-located, preferred for constructor-style promises
$promise = new Promise(function ($resolve, $reject, $onCancel) {
    $timerId = Loop::addTimer(10.0, fn() => $resolve('done'));
    $onCancel(fn() => Loop::cancelTimer($timerId)); // right next to the work
});

$promise->cancel(); // timer is cancelled, no callback fires

// Via ->onCancel() — preferred for deferred-style promises
$promise = new Promise();
$timerId = Loop::addTimer(10.0, fn() => $promise->resolve('done'));
$promise->onCancel(fn() => Loop::cancelTimer($timerId));

$promise->cancel(); // timer is cancelled, no callback fires

// Incorrect — timer keeps running after cancel()
$promise = new Promise(function ($resolve) {
    Loop::addTimer(10.0, fn() => $resolve('done'));
    // No onCancel registered — cancelling changes state but not the timer
});

$promise->cancel(); // Promise state changes, but timer is still live

$promise->onCancel(function () {
    throw new \RuntimeException('cleanup failed');
});

// cancel() propagates the exception — not a silent no-op
try {
    $promise->cancel();
} catch (\RuntimeException $e) {
    echo $e->getMessage(); // "cleanup failed"
}

$promise->onCancel(function () {
    throw new \RuntimeException('handler A failed');
});

$promise->onCancel(function () {
    echo "handler B ran\n"; // always runs — all handlers execute
});

try {
    $promise->cancel();
} catch (\RuntimeException $e) {
    echo $e->getMessage(); // "handler A failed"
}

var_dump($promise->isCancelled()); // true — state is cancelled regardless

// Wrong — exception leaks out of cancel()
$promise->onCancel(function () use ($conn) {
    $conn->close(); // may throw
});

// Correct — failures are contained
$promise->onCancel(function () use ($conn) {
    try {
        $conn->close();
    } catch (\Throwable) {
        // log if needed, but never let it propagate
    }
});

$promise->onCancel(function () use ($requestId) {
    // Correct: fire and return immediately
    Loop::addCurlRequest(
        "https://api.example.com/cancel/$requestId",
        [],
        fn() => null
    );

    // Wrong: do not await or block inside onCancel
    // Http::delete("https://api.example.com/cancel/$requestId")->wait();
});

$root = downloadFile($url);
$processed = $root->then(fn($file) => processFile($file));

$root->cancel(); // Cancels root AND processed (forward propagation)

$processed = downloadFile($url)->then(fn($file) => processFile($file));

// Don't have $root? cancelChain() finds it for you.
$processed->cancelChain(); // Cancels download AND processed

// No onCancel() handler — cancelling changes state but not the timer
function nonCancellableDelay(float $seconds): PromiseInterface
{
    return new Promise(function (callable $resolve) use ($seconds) {
        Loop::addTimer($seconds, function () use ($resolve) {
            $resolve();
        });
    });
}

// onCancel() handler registered — cancelling also cancels the timer
function cancellableDelay(float $seconds): PromiseInterface
{
    return new Promise(function (callable $resolve, callable $reject, callable $onCancel) use ($seconds) {
        $timerId = Loop::addTimer($seconds, fn() => $resolve());
        $onCancel(fn() => Loop::cancelTimer($timerId));
    });
}

$start = microtime(true);
$promise = nonCancellableDelay(5);
Loop::addTimer(1, $promise->cancel(...));
Loop::run();
echo 'Non-cancellable: ' . round(microtime(true) - $start, 2) . 's' . PHP_EOL;
// Non-cancellable: 5.01s — loop waited for the timer even though the
// promise was cancelled

$start = microtime(true);
$promise = cancellableDelay(5);
Loop::addTimer(1, $promise->cancel(...));
Loop::run();
echo 'Cancellable: ' . round(microtime(true) - $start, 2) . 's' . PHP_EOL;
// Cancellable: 1.00s — timer was cancelled immediately, loop exited cleanly

$derived = downloadFile($url)
    ->then(fn($file) => parseFile($file))
    ->then(fn($data) => validateData($data));

// Without this, cancelling $derived only cancels the validateData step.
// The download and parse are still in flight.
$derived = Promise::propagateCancellation($derived);

// Now cancelling $derived walks up and cancels the download too.
$derived->cancel();

$job = Promise::propagateCancellation(
    buildReport()->then(fn($r) => renderReport($r))
);

$job->cancel(); // cancels all the way back to buildReport()

$controller = new Promise(); // acts as an abort signal
$work        = startLongRunningJob();

// Cancelling $controller will also cancel $work
Promise::forwardCancellation($controller, $work);

// Later, when the user clicks "cancel":
$controller->cancel(); // $work is also cancelled, its onCancel() handlers fire

$signal = new Promise(); // shared abort signal

Promise::forwardCancellation($signal, fetchUsers());
Promise::forwardCancellation($signal, fetchOrders());
Promise::forwardCancellation($signal, fetchStats());

// One call cancels all three operations
$signal->cancel();

$target = null;

// Target is assigned later, conditionally
if ($condition) {
    $target = startOptionalWork();
}

Promise::forwardCancellation($source, $target); // safe even if $target is null

$commit = Promise::uninterruptible(
    $db->commit() // must complete even if the caller gives up waiting
);

// User cancels their request after 2 seconds
delay(2.0)->then(fn() => $commit->cancel());

// The DB commit still runs to completion. Calling cancel() only
// discards the mirror; the internal $db->commit() promise is unaffected.

function transferFunds(Account $from, Account $to, int $amount): PromiseInterface
{
    return $db->beginTransaction()
        ->then(fn() => $from->debit($amount))
        ->then(fn() => $to->credit($amount))
        ->then(fn() => Promise::uninterruptible($db->commit()))
        ->catch(fn($e) => Promise::uninterruptible($db->rollback())
            ->then(fn() => throw $e)
        );
}

$result->isFulfilled(); // bool
$result->isRejected();  // bool
$result->isCancelled(); // bool

$result->value;  // mixed — only meaningful when isFulfilled()
$result->reason; // mixed — only meaningful when isRejected()

json_encode($result);
// {"status":"fulfilled","value":"..."}
// {"status":"rejected","reason":{"message":"...","class":"...","file":"...","line":...}}
// {"status":"cancelled"}

// Array
Promise::all([$promise1, $promise2, $promise3]);

// Generator
Promise::all((function () {
    yield 'users'  => fetchUsers();
    yield 'orders' => fetchOrders();
    yield 'stats'  => fetchStats();
})());

// Any Traversable
Promise::all(new ArrayIterator([$promise1, $promise2]));

// Correct
Promise::all([$promise1, $promise2]);

// Wrong — do not call static methods on instances
$promise->all([$promise1, $promise2]);

$userPromise  = fetchUser(1);
$orderPromise = fetchOrders(1);
$statsPromise = fetchStats(1);

$userPromise->onCancel(fn()  => closeUserConnection());
$orderPromise->onCancel(fn() => closeOrderConnection());
$statsPromise->onCancel(fn() => closeStatsConnection());

$all = Promise::all([$userPromise, $orderPromise, $statsPromise]);

// Scenario 1: fetchUser rejects
// -> $orderPromise and $statsPromise are auto-cancelled synchronously
// -> their onCancel() handlers run immediately
// -> $all rejects with the user fetch error

// Scenario 2: $orderPromise is cancelled externally
// -> $all rejects with CancelledException
// -> $userPromise and $statsPromise are auto-cancelled synchronously
// -> their onCancel() handlers run immediately

// Scenario 3: $all itself is cancelled
// -> all three promises are auto-cancelled synchronously
// -> all three onCancel() handlers run before cancel() returns
$all->cancel();

Promise::all([
    'user'   => fetchUser(1),
    'orders' => fetchOrders(1),
    'stats'  => fetchStats(1),
])->then(function (array $results) {
    $user   = $results['user'];
    $orders = $results['orders'];
    $stats  = $results['stats'];
})->catch(function (\Throwable $e) {
    // One rejected or was cancelled
    // Remaining pending promises were automatically cancelled synchronously
    echo "Failed: " . $e->getMessage();
});

Promise::allSettled([
    'primary'   => fetchFromPrimary(),
    'secondary' => fetchFromSecondary(),
    'fallback'  => fetchFromFallback(),
])->then(function (array $results) {
    foreach ($results as $key => $result) {
        if ($result->isFulfilled()) {
            echo "$key succeeded: " . json_encode($result->value) . "\n";
        } elseif ($result->isRejected()) {
            echo "$key failed: " . $result->reason->getMessage() . "\n";
        } elseif ($result->isCancelled()) {
            echo "$key was cancelled\n";
        }
    }
});

Promise::race([
    fetchFromRegionA(),
    fetchFromRegionB(),
    fetchFromRegionC(),
])->then(function ($result) {
    // Fastest settled — the other two were already cancelled synchronously
    echo "Fastest result: $result\n";
})->catch(function (\Throwable $e) {
    // The first to settle rejected or was cancelled
    // All others were already cancelled synchronously
});

Promise::any([
    tryPrimaryDatabase(),
    tryReplicaDatabase(),
    tryFallbackDatabase(),
])->then(function ($result) {
    echo "Got data: " . json_encode($result) . "\n";
})->catch(function (\Hibla\Promise\Exceptions\AggregateErrorException $e) {
    // Every single promise rejected or was cancelled
    foreach ($e->getErrors() as $index => $error) {
        echo "Source $index: " . $error->getMessage() . "\n";
    }
});

$query = slowDatabaseQuery();
$query->onCancel(fn() => cancelQuery());

Promise::timeout($query, seconds: 5.0)
    ->then(fn($result) => processResult($result))
    ->catch(function (\Throwable $e) {
        if ($e instanceof \Hibla\Promise\Exceptions\TimeoutException) {
            echo "Query timed out — query was cancelled\n";
        } else {
            echo "Query failed: " . $e->getMessage() . "\n";
        }
    });

// Correct — factory callables, tasks start when the library decides
$tasks = [
    fn() => fetchUser(1),
    fn() => fetchUser(2),
    fn() => fetchUser(3),
];

// Incorrect — all three are already running before concurrent() sees them
$tasks = [
    fetchUser(1),
    fetchUser(2),
    fetchUser(3),
];

$tasks = [
    'alice' => fn() => fetchUser('alice'),
    'bob'   => fn() => fetchUser('bob'),
    'carol' => fn() => fetchUser('carol'),
];

Promise::concurrent($tasks, concurrency: 2)
    ->then(fn(array $results) => print(count($results) . " users fetched\n"))
    ->catch(fn($e) => print("Aborted: " . $e->getMessage()));

> Promise::concurrent((function () use ($db) {
>     foreach ($db->cursor('SELECT id FROM records') as $row) {
>         yield $row['id'] => fn() => processRecord($row['id']);
>     }
> })(), concurrency: 10);
> 

$tasks = [
    'alice' => fn() => syncUser('alice'),
    'bob'   => fn() => syncUser('bob'),
    'carol' => fn() => syncUser('carol'),
];

Promise::concurrentSettled($tasks, concurrency: 2)
    ->then(function (array $results) {
        $succeeded = array_filter($results, fn($r) => $r->isFulfilled());
        $failed    = array_filter($results, fn($r) => $r->isRejected());
        $cancelled = array_filter($results, fn($r) => $r->isCancelled());

        echo count($succeeded) . " synced, "
           . count($failed)    . " failed, "
           . count($cancelled) . " cancelled\n";
    });

$emails = [
    '[email protected]' => fn() => sendEmail('[email protected]'),
    '[email protected]'   => fn() => sendEmail('[email protected]'),
    '[email protected]' => fn() => sendEmail('[email protected]'),
    '[email protected]'  => fn() => sendEmail('[email protected]'),
    '[email protected]'   => fn() => sendEmail('[email protected]'),
];

Promise::batch($emails, batchSize: 2, concurrency: 2)
    ->then(fn(array $results) => print(count($results) . " emails sent\n"));

$records = [
    1 => fn() => importRecord(1),
    2 => fn() => importRecord(2),
    3 => fn() => importRecord(3),
    4 => fn() => importRecord(4),
    5 => fn() => importRecord(5),
];

Promise::batchSettled($records, batchSize: 2, concurrency: 2)
    ->then(function (array $results) {
        $failed = array_filter($results, fn($r) => $r->isRejected());
        if (count($failed) > 0) {
            retryFailed($failed);
        }
    });

$ids = [1 => 1, 2 => 2, 3 => 3];

Promise::map($ids, fn($id) => fetchUser($id), concurrency: 2)
    ->then(fn(array $users) => processAll($users));

$records = [1 => $record1, 2 => $record2, 3 => $record3];

Promise::mapSettled($records, fn($record) => processRecord($record), concurrency: 2)
    ->then(function (array $results) {
        $processed = array_filter($results, fn($r) => $r->isFulfilled());
        $skipped   = array_filter($results, fn($r) => $r->isRejected());
        echo count($processed) . " processed, " . count($skipped) . " skipped\n";
    });

$products = [
    'sku-1' => $product1,
    'sku-2' => $product2,
    'sku-3' => $product3,
];

Promise::filter($products, fn($product) => checkInStock($product), concurrency: 2)
    ->then(fn(array $inStock) => print(count($inStock) . " available\n"));

Promise::filter($products, function ($product) {
    return validate($product)
        ->catch(fn() => false); // validation failure = excluded, not aborted
});

Promise::reduce(
    [1, 2, 3, 4, 5],
    fn($carry, $n) => Promise::resolved($carry + $n),
    initial: 0
)->then(fn($sum) => print("Sum: $sum\n")); // Sum: 15

$records = [$record1, $record2, $record3];

Promise::forEach($records, fn($record) => saveToExternalApi($record), concurrency: 2)
    ->then(fn() => print("All records processed\n"))
    ->catch(fn($e) => print("Aborted: " . $e->getMessage()));

$users = [$user1, $user2, $user3];

Promise::forEachSettled($users, fn($user) => sendWelcomeEmail($user), concurrency: 2)
    ->then(fn() => print("All sends attempted\n"));

use Hibla\Promise\Promise;

Promise::setRejectionHandler(function (mixed $reason, $promise) {
    logger()->error('Unhandled rejection', ['reason' => $reason]);
});

// Restore default throw behavior
Promise::setRejectionHandler(null);

$promise->catch(fn($e) => logError($e));       // intentional
$promise->then(null, fn($e) => logError($e));  // same

$promise = Promise::rejected(new \RuntimeException('Something went wrong'));

// Accessing the reason marks the promise as "accessed"
$reason = $promise->reason; // sets valueAccessed = true

// No catch() attached — but the exception is never thrown on destruct.
// The rejection is silently swallowed.

// Wrong — inspection silences the throw
if ($promise->isRejected()) {
    $reason = $promise->reason;
    // Exception never thrown on destruct — rejection is silent
}

// Correct — attach a handler to keep tracking active
$promise->catch(function (\Throwable $e) {
    logger()->error($e->getMessage());
});

/** @return PromiseInterface<void> */
function sendEmail(): PromiseInterface
{
    return Promise::resolved();
}

Promise::resolved();        // PromiseInterface<void>
Promise::resolved(42);      // PromiseInterface<int>
Promise::resolved('hello'); // PromiseInterface<string>
Promise::resolved($user);   // PromiseInterface<User>