Skip to content

Chained promises

Greg Bowler edited this page May 4, 2026 · 3 revisions

Once a promise exists, we usually build a chain of callbacks around it.

The chain methods

  • then() runs when the promise resolves.
  • catch() runs when the promise rejects.
  • finally() runs in either case.

This implementation keeps returning the same promise instance throughout the chain. This is slightly different to how Promises within the browser context are handled - the chaining behaviour still works, but it is implemented by mutating the promise's internal chain rather than allocating a new promise object on every call.

Forwarding values

If a then() callback returns a scalar or object, that value becomes the input to the next compatible then() callback.

$promise
	->then(function(string $value) {
		return strtoupper($value);
	})
	->then(function(string $value) {
		echo $value, PHP_EOL;
	});

If a callback returns null, the current chain stops there.

Returning another promise

If a then(), catch(), or finally() callback returns another PromiseInterface, the chain waits for that promise to settle and continues with its resolved value or rejection.

$promise
	->then(function(int $userId) use ($api) {
		return $api->loadUser($userId);
	})
	->then(function(User $user) {
		echo $user->name, PHP_EOL;
	});

This is the correct way to combine asynchronous steps. We should not resolve a promise directly with another promise. If resolve() receives a PromiseInterface, the package rejects with PromiseResolvedWithAnotherPromiseException.

Typed callbacks

This package uses PHP parameter types when deciding which callback should run next.

$promise
	->then(function(string $value) {
		return 42;
	})
	->then(function(float $number) {
		echo "This will be skipped, because a float is never returned", PHP_EOL;
	})
	->then(function(int $number) {
		echo $number, PHP_EOL;
	});

The float handler is skipped because the resolved value is an int. The int handler then receives the value.

The same idea applies to catch() callbacks, which means we can target specific exception types:

$promise
	->catch(function(ValueError $error) {
		echo "Value error: ", $error->getMessage(), PHP_EOL;
	})
	->catch(function(ArithmeticError $error) {
		echo "Arithmetic error: ", $error->getMessage(), PHP_EOL;
	});

If no matching catch() handles a rejection, the exception bubbles out to the main thread.

If a matching catch() returns a value, the chain becomes resolved again and later then() callbacks can continue:

$promise
	->catch(function(RuntimeException $error) {
		return "Fallback value";
	})
	->then(function(string $value) {
		echo $value, PHP_EOL;
	});

finally() behaviour

finally() receives the resolved value or rejected reason, and always runs after the promise settles.

  • If it returns a scalar, the original resolution or rejection still continues.
  • If it returns another promise, the chain waits for that promise.
  • If it throws, the thrown exception leaves the chain.

Use finally() for cleanup, logging, or state updates that should happen regardless of success or failure.


To make all of this work, we need an Async event loop.

Clone this wiki locally