await syntax, they can eliminate the need for callbacks altogether.
There’s just one catch.
Everything worth doing takes time.
— Bob Dylan
When building real-world apps, you’re going to need to interact with the outside world. For example, you may need to respond to user input, make requests to HTTP servers, or wait for timers — in other words, you’ll need to perform IO operations.
To get a feel for this, consider this script to draw a simple animation:
drawKeyFrame(1); wait(1000); drawKeyFrame(2); wait(1000); drawKeyFrame(0);
In a multi-threaded language like Python, Ruby, or C#, the runtime can keep doing its thing while the script waits between keyframes. It can continue to respond to user input and draw the animation’s intermediate frames.
onclick handler finishes executing.
When calling an IO-related function like
setTimeout(), you’ll usually pass in a callback that the browser will store until needed. Then when an event of interest occurs, such as a timeout or HTTP response, the browser can handle it by executing the stored callback function.
The important thing to note here is that when you start the IO operation, the browser doesn’t wait for it to complete before continuing. The script just keeps on executing. It’s only after the original script has fully executed that the browser can execute the callback and respond to the event.
This means that your code is asynchronous; it’s executed out of order. You can see this in action in the following example, where the last
console.log() statement is executed before the the middle one — even though the middle one is scheduled to execute first!
With practice, you can get used to dealing with individual callbacks that only do one or two things. But unfortunately, the real world is complicated. Sometimes you’ll need to combine callbacks, and that’s when shit turns nasty.
For example, imagine that you’re building a looping animation using
setTimeout() and CSS transitions. During each keyframe, you’ll need to add or remove some CSS classes, and then call
setTimeout() to schedule the next keyframe.
Here’s what this would look like with just 3 keyframes:
Looks awful, right? But imagine for a moment that instead of 3 keyframes, you had 10… the nested
setTimeout() statements would result in so much whitespace that you could build a pyramid! This is what people call callback hell. And in callback-based code, these pyramids appear everywhere that you need to interact with anything of importance - reading and writing files, interacting with a server, the list goes on.
Promises, together with the
await syntax, allow you to write code that executes in the order you’d expect, while not causing the browser to freeze.
For example, here’s how you’d implement the above animation using promises,
Did you notice how similar the
animate() function is to the first example? It almost looks like something you’d see in a multi-threaded language. Promises and
await give you the performance benefits of asynchronous code, without the loss of clarity.
But I’m getting ahead of myself — we haven’t even covered what exactly a promise is. So let’s take a look.