The Basic Concepts
A promise represents a value that is not yet known while a deferred represents work that is not yet finished.
A promise is a placeholder for the initially unknown result of the asynchronous code while a deferred represents the code which is going to be executed to receive this result. Every deferred has its own promise which works as a proxy for the future result. While a promise is a result returned by some asynchronous code, a deferred can be resolved or rejected by it’s caller, so we can separate the promise from the resolver.
Create a deferred object:
A promise for this deferred can be retrieved with
promise() method, which returns an instance of the
A promise has three possible states:
- unfulfilled - the promise starts with this state because the value of the deferred is yet unknown
- fulfilled - the promise is filled with the value returned from the deferred
- failed - there was an exception during the deferred execution.
A deferred object has two methods to change the state of its promise:
resolve($value = null)when the code executes successfully, changes the state to fulfilled
reject($reason = null)the code execution fails, changes the state to failed
Promises provide methods only to attach additional handlers to the appropriate states (
always), but you cannot manually change the state of a promise. For example, we can attach onFulfilled handler via
done() method and then call it once the deferred is resolved:
To handle the failed state we can use
otherwise(callable $onRejected) method to add a handler to a promise and
reject($reason = null) method of the deferred object:
Notice that we can use
done() method to add all three handlers to the promise. For example, the previous example can be rewritten with
done() method instead of
Here we register handlers for both fulfilled and failed states.
A promise can change its state from unfulfilled to either fulfilled or failed but not vice versa. After resolution or rejection, all observers are notified. Once the promise has been resolved or rejected it cannot change its state or the result value.
We can give a promise to any number of consumers and each of them will observe the resolution of the promise independently. A deferred can be given to any number of producers and the promise will be resolved by the one which first resolves it.
Promises can be chained, when the return value of each promise is forwarded to the next promise in the chain. That means that the next promise in the chain will be invoked with this resolved value.
Forwarding can be done with two methods:
then(callable $onFulfilled = null, callable $onRejected = null)for resolution forwarding
otherwise(callable $onRejected)for rejection forwarding
then() registers new fulfilled and rejection handlers and returns a new promise. This promise will fulfill with the return value of either
$onRejected, whichever is called or will reject with the thrown exception.
We can build a pipe of promises when each call to
then() returns a new promise that will resolve with the return value of the previous handler:
The result of this code will be following:
In each promise,
$onFulfilled handler outputs the received value, changes it and then passes it to the next promise in the chain.
otherwise(callable $onRejected) registeres a rejection handler for promise. Under the hood, this method is simply a shortcut for:
Rejected promises work like chained
try/catch blocks. When you catch an exception, you must rethrow it to the next promise:
This example looks very similar to the previous one. But now we are throwing exceptions instead of returning values. Notice that the first handler receives
mixed $data from the
$deferred->reject() method and not the exception. The output will be the following:
Additionally, you can type hint the
$reason argument of
$onRejected handler to catch only specific errors:
In this snippet the third handler will be skipped:
You can also mix resolution and rejection forwardings like this:
The code above outputs the following:
Then vs Done
The rule of thumb is:
Either return your promise or call
At a first glance, both
done() methods look very similar, but there is a significant difference between them.
then() transofrms a promise’s value and returns a new promise for this transformed value. So, we can chain
then() calls. This method also allows to recover from or propagate intermediate errors. Any errors that are not handled will be caught by the promise and used to reject the promise returned by
done() consumes the promise’s value or handles the error.
done() always returns
null. When we call
done() all responsibility for errors lies on us. Any error (either a thrown exception or returned rejection) in the
$onRejected handlers will be rethrown in an uncatchable way causing a fatal error:
The promise itself doesn’t make your code execution asynchronous. A promise is a placeholder for a result which is initially unknown while a deferred represents the computation that results in the value. A deferred can be resolved or rejected by the caller, so the promise is separated from the resolver. With promises you can write your asynchronous code in a synchronous way to make it more readable, this means that instead of using callbacks we can return a value (promise).
You can find examples from this article on GitHub.
This article is a part of the ReactPHP Series.