What are JavaScript Promises & How to Use Them
Javascript's problem
JavaScript has a problem: it's single-threaded.
What do we mean by that? If something takes time, the JS engine can't do anything else in the meanwhile.
Occasionally when coding a "while" block I forget to increment the variable in the end condition. This creates an infinite loop. When I do this … everything freezes. The browser doesn't respond anymore. I have to force the window to close. This is an extreme case. But let's step back a little. Movies run at 24 frames per second, and video games at 60 FPS or more. 24 frames per second mean one frame every 40 milliseconds or so. (1/24 = 0.0416 seconds).
For a fluid experience, any process in JavaScript must get the job within 40 milliseconds or less. Otherwise, users get the impression the application is lagging.
If you want to call an API on a web server, the message's round trip is bound to take longer than that. But the browser cannot afford to wait.
So we need to find ways to tell the code to do other stuff while it is waiting.
Callback functions
The first solution was to tell the browser: "Run this process (for example, call this API), and when it's done, let me know". Or more precisely: on completion execute the function provided (as a parameter in a function call). This function we want the code to call back is what is called… a "callback".
This callback function allows us to add further logic in reaction to the server's response. For example: when the user clicks on the button, first log in by calling the login API. When the server responds, retrieve the authentication data and interpret it. Then call a web service with the login data to update the player's information.
In this example alone, there are three callbacks in play. The first one responds to the button press. The second manages the call and response to the login API. And the last one retrieves the response from the second web service. Bear in mind this example is neither far-fetched nor complex.
But this kind of process produces callbacks within callbacks within callbacks. The code quickly becomes unreadable, it produces "callback hell".
Another problem arises with this kind of operation: error handling.
Imagine you have specific processing to carry out at each stage or depth of the callback stack. Imagine now that an error occurs. If it is not managed with a try/catch, the error message is useless. "An error occurred at line 5 of the anonymous function".
That's without even touching on the cases when the code fails silently.
The only solution is to add try/catch management at every depth, on every callback call. This quickly becomes *very *unwieldy.
What is a promise?
To try to solve these problems a new kind of object was invented. It has three fundamental properties:
- It encapsulates asynchronous processing
- It can be chained, without the need to nest (callback) functions.
- It handles errors
This object is the Promise.
A Promise has two functions that can be called on it: "then" and "catch". The "then" function takes the result of the resolution as a parameter, and returns a new promise. The "catch" function takes the error as a parameter and also returns a new promise.
The fact these functions return Promises is precisely what allows us to "chain" them, i.e. to do:
p.then(function1).then(function2)
where p
is a promise. Chaining allows us to read code sequentially, instead of diving deeper and deeper into nested callback functions.
Promises trigger the first "catch" regardless of which step throws an error. This allows centralized (and sane!) error management.
How to make a promise
Let's code a function that creates a promise that waits for a specified time. Let's call that function sleep.
The Promise's constructor takes a function.
This function takes two functions as parameters, typically called "resolve" and "reject". We call these when the process ends. Either it succeeds, in which case we call "resolve", or it fails and we call "reject".
Our sleep function looks like this:
const sleep = (nb) => {
return new Promise ((resolve, reject) => {
setTimeout(() => resolve(), nb) ;
})
};
The second parameter, reject, is unused here. We could omit it, but we'll keep it just to be complete. Now if we do:
sleep(1000).then(() => console.log("done"));
Sleep produces a promise that triggers a function after a certain time.
Promises are good but we can do better!
I have good news. There's an even better way to call promises.
It might feel different since the "then" function is never explicitly called. Instead, this syntax relies on the async/await keywords. But all it does is allow us to call promises. How does it work?
Well, let's take an example of a basic promise sequence using our sleep promise:
const wait = () => {
sleep(1000)
.then(() => sleep(1000))
.then(() => console.log("2s later");
};
Using the async/await notation we can write the same thing:
const wait = async () => {
await sleep(1000);
await sleep(1000);
console.log("2s later") ;
} ;
There's only a difference in syntax. The same Promise objects are being called. If we wrap the two await calls in a try/catch, it will handle errors just like synchronous code does.
All this transforms the way of writing promises. And it closely resembles how we write synchronous code. Even if we are in fact calling Promises. The wait function defined above also returns a Promise.
Key Takeways
JavaScript is all about interacting with interfaces and APIs. Callbacks used to be the preferred way of doing this, but they have many drawbacks. Promises are a far better solution. The async/await syntax uses Promises to describe asynchronous behavior while staying readable. And allow us to escape "callback hell".
We help you better understand software development. Receive the latest blog posts, videos, insights and news from the frontlines of web development