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();
});
$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();
Loop::addTimer(1.0, function () use ($promise) {
$promise->resolve('Done after 1 second');
});
$promise->then(fn($value) => print($value));
$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();
$barrier = new CountdownLatch(3);
$bus->on('worker.done', function () use ($barrier, $promise) {
$barrier->countDown();
if ($barrier->isDone()) {
$promise->resolve();
}
});
$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 = 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
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();
$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();
});
$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.
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";
}
});
$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());
});