Promises
A promise is an object which can be returned synchronously from an asynchronous function (ref).
Promises can be used to avoid callback hell, and they are more and more frequently encountered in modern JavaScript projects.
Analogy
Imagine that you’re a top singer, and fans ask day and night for your upcoming single.
To get some relief, you promise to send it to them when it’s published. You give your fans a list. They can fill in their email addresses, so that when the song becomes available, all subscribed parties instantly receive it. And even if something goes very wrong, say, a fire in the studio, so that you can’t publish the song, they will still be notified.
Everyone is happy:
- you, because the people don’t crowd you anymore,
- fans, because they won’t miss the single.
This is a real-life analogy for things we often have in programming:
- A “producing code” that does something and takes time. For instance, a code that loads the data over a network. That’s a “singer”.
- A “consuming code” that wants the result of the “producing code” once it’s ready. Many functions may need that result. These are the “fans”.
- A promise is a special JavaScript object that links the “producing code” and the “consuming code” together. In terms of our analogy: this is the “subscription list”. The “producing code” takes whatever time it needs to produce the promised result, and the “promise” makes that result available to all of the subscribed code when it’s ready.
The analogy isn’t terribly accurate, because JavaScript promises are more complex than a simple subscription list: they have additional features and limitations. But it’s fine to begin with.
Sample code
const fetchingPosts = new Promise((res, rej) => {
$.get("/posts")
.done(posts => res(posts))
.fail(err => rej(err));
});
fetchingPosts
.then(posts => console.log(posts))
.catch(err => console.log(err));
Explanation
When you do an Ajax request the response is not synchronous because you want a resource that takes some time to come. It even may never come if the resource you have requested is unavailable for some reason (404).
To handle that kind of situation, ES2015 has given us promises. Promises can have three different states:
- Pending
- Fulfilled
- Rejected
Let's say we want to use promises to handle an Ajax request to fetch the resource X.
Create the promise
We firstly are going to create a promise. We will use the jQuery get method to do our Ajax request to X.
const xFetcherPromise = new Promise( // Create promise using "new" keyword and store it into a variable
function(resolve, reject) { // Promise constructor takes a function parameter which has resolve and reject parameters itself
$.get("X") // Launch the Ajax request
.done(function(X) { // Once the request is done...
resolve(X); // ... resolve the promise with the X value as parameter
})
.fail(function(error) { // If the request has failed...
reject(error); // ... reject the promise with the error as parameter
});
}
)
As seen in the above sample, the Promise object takes an executor function which takes two parameters resolve and reject. Those parameters are functions which when called are going to move the promise pending state to respectively a fulfilled and rejected state.
The promise is in pending state after instance creation and its executor function is executed immediately. Once one of the function resolve or reject is called in the executor function, the promise will call its associated handlers.
new Promise(executor);
Executor function: A function that is passed with the arguments resolve and reject. The executor function is executed immediately by the Promise implementation, passing resolve and reject functions (the executor is called before the Promise constructor even returns the created object). The resolve and reject functions, when called, resolve or reject the promise, respectively. The executor normally initiates some asynchronous work, and then, once that completes, either calls the resolve function to resolve the promise or else rejects it if an error occurred. If an error is thrown in the executor function, the promise is rejected. The return value of the executor is ignored.
handler: It refers to
.then()
or.catch()
.
Promise handlers usage
To get the promise result (or error), we must attach to it handlers by doing the following:
xFetcherPromise
.then(function(X) {
console.log(X);
})
.catch(function(err) {
console.log(err)
})
If the promise succeeds, resolve is executed and the function passed as .then
parameter is executed.
If it fails, reject is executed and the function passed as .catch
parameter is executed.
Note : If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached. (Ref: MDN)
Example: callback to promise
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
The browser allows to track the loading of external resources – scripts, iframes, pictures and so on. There are two events for it:
- onload – successful load,
- onerror – an error occurred.
Let’s rewrite it using Promises.
The new function loadScript
will not require a callback. Instead, it will create and return a Promise object that resolves when the loading is complete. The outer code can add handlers (subscribing functions) to it using .then
:
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
Usage:
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));
We can immediately see a few benefits over the callback-based pattern:
Symbols | Meaning |
---|---|
Promises allow us to do things in the natural order. First, we run loadScript(script), and .then we write what to do with the result. | We must have a callback function at our disposal when calling loadScript(script, callback). In other words, we must know what to do with the result before loadScript is called. |
We can call .then() on a Promise as many times as we want. Each time, we’re adding a new “fan”, a new subscribing function, to the “subscription list”. | There can be only one callback. |
Example: promise chaining
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// use functions declared in scripts
// to show that they indeed loaded
one();
two();
three();
});
This code can be made bit shorter with arrow functions:
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// scripts are loaded, we can use functions declared there
one();
two();
three();
});
Here each loadScript
call returns a promise, and the next .then
runs when it resolves. Then it initiates the loading of the next script. So scripts are loaded one after another.
We can add more asynchronous actions to the chain. Please note that code is still “flat”, it grows down, not to the right. There are no signs of “pyramid of doom”.
Newbie's mistake
The whole promise chain works, because a call to promise.then returns a promise, so that we can call the next .then on it.
When a handler returns a value, it becomes the result of that promise, so the next .then is called with it. As the result is passed along the chain of handlers, we can see a sequence of alert calls: 1 → 2 → 4.
new Promise(function(resolve, reject) {
// This is the executor area!
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
A classic newbie error: technically we can also add many .then to a single promise. This is not chaining instead, it is just several handlers to one promise. They don’t pass the result to each other, instead they process it independently.
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
Error handling
Implicit try-catch
Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler. That’s very convenient in practice.
The code of a promise executor and promise handlers has an "invisible try..catch" around it. If an exception happens, it gets caught and treated as a rejection.
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!
…Works exactly the same as this:
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!
The "invisible try..catch
" around the executor automatically catches the error and turns it into rejected promise.
This happens not only in the executor function, but in its handlers as well. If we throw inside a .then
handler, that means a rejected promise, so the control jumps to the nearest error handler. (When we say handlers of Promise, usually it refers to the .then
handler.)
Here’s an example:
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("Whoops!"); // rejects the promise
}).catch(alert); // Error: Whoops!
This happens for all errors, not just those caused by the throw statement. For example, a programming error:
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
blabla(); // no such function
}).catch(alert); // ReferenceError: blabla is not defined
The final .catch
not only catches explicit rejections, but also occasional errors in the handlers above.
Rethrowing
In the example below we see the other situation with .catch
. The handler (*)
catches the error and just can’t handle it (e.g. it only knows how to handle URIError
), so it throws it again:
// the execution: catch -> catch -> then
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// handle it
} else {
alert("Can't handle such error");
> throw error; // throwing this or another error jumps to the next catch
}
}).then(function() {
/* doesn't run here */
}).catch(error => { // (**)
alert(`The unknown error has occurred: ${error}`);
// don't return anything => execution goes the normal way
});
The execution jumps from the first .catch (*)
to the next one (**)
down the chain.
Unhandled rejections
>window.addEventListener('unhandledrejection', function(event) {
> // the event object has two special properties:
> alert(event.promise); // [object Promise] - the promise that generated the error
> alert(event.reason); // Error: Whoops! - the unhandled error object
>});
new Promise(function() {
throw new Error("Whoops!");
}); // no catch to handle the error
The event is the part of the HTML standard.
If an error occurs, and there’s no .catch
, the unhandledrejection
handler triggers, and gets the event
object with the information about the error, so we can do something.
Usually such errors are unrecoverable, so our best way out is to inform the user about the problem and probably report the incident to the server.
In non-browser environments like Node.js there are other ways to track unhandled errors.