If you head over to the ECMAScript 6 documentation, you’ll see that under Promises lies one sentence. “First class representation of a value that may be made asynchronously and be available in the future.” Are you wondering what that means?
Yeah, me too. It’s probably this, among many other misunderstandings about how JavaScript executes code under the hood, that has contributed to the confusion surrounding the Promise feature in JavaScript.
Well, in this blog post, we’ll aim to demystify exactly how this feature works so you can implement Promises into your code like a pro. To get to the Promised land (sorry, not sorry), we first need to understand how JavaScript handles synchronous and asynchronous code. We’ll then dive into what Promises are, how they’re helpful, and why you might want to start using them in your code if you haven’t already.
JavaScript is a single-threaded programming language which means that it has a single call stack upon which it can push new function execution contexts. In other words, it can do one thing at a time.
Not only that, but it does it with zero regards for whether the previous code has finished executing further up your .js file, making it synchronous. “Ok,” you exclaim, “but if that’s true, how can I both click buttons that do stuff on a browser while I’m waiting for other code to load? How can it be that I can do many actions, some of which appear to have blocking functionality simultaneously while other code runs?”. Great questions, thanks for asking.
Well, in short, it’s the browser or any other higher-level runtime that runs JavaScript allowing you to do these things. These higher-level abstractions give us other data structures similar to the call stack, allowing us to store code to be run later or at different times.
Under the hood, the browser uses these abstractions to manage what gets put onto our call stack. Two examples of these other data structures being used in conjunction with our call stack are setTimeout()
that applies an input time constraint to when a callback is executed or a Fetch request that waits for the receipt of a response from a server before processing the following line of code.
Both are incompatible with JavaScript’s basic single threadedness, but the abstractions give us additional powers that allow for what appears to be non-single threaded behavior. In Chromium, these tools are known as WebAPIs and in Node C++ APIs. They juggle the code asynchronously — ensuring it doesn’t interrupt code executed in the main thread of execution. You might be thinking at this stage, “alright, then that means that this is executed in another thread”.
But that depends on the engine executing your JavaScript. In the case of Chromium, this is simply the illusion of what appears like multithreaded concurrently executed code. We won’t go into any more detail on this since it warrants its own set of posts by itself, but this should give us enough context to have an understanding of what these higher leveled abstractions are capable of in JavaScript!
Ok, so now we’ve got our understanding down for asynchronicity, let’s now move back onto the star of the show, Promises.
A Promise is a type of JavaScript object representing a “Promised” future outcome. That’s it. While this isn’t a very clear description — especially if you’re just piecing together your mental model of Promises, let’s focus on the JavaScript object part of that definition I just shared. At the most basic level, it’s a series (or chain) of objects within which there are callbacks. Chain because they are connected and if one link breaks the entire Promise does. Practically speaking, it allows you to do a few things, let’s take a look at those:
First, you can set up code that relies upon executing and completing other callbacks in a stepped or blocked manner.
This is very powerful since it gives you the capability to closely control the flow of execution, which is terrific when you want to interact with a server, execute code upon the successful receipt of data, or create throttle functionality to avoid your server being pwned.
Examples of this in action might include rendering your feed items on Instagram after the page loads or having Netflix play a video once sufficient data has been downloaded into your client.
Setting up asynchronous tasks to run-one-after-the-other-is-completed is known as Promise chaining, typically put in practice using the .then
syntax. Yes, there’s the specific syntax to learn here, but believe me, this is a small price to pay when the alternative is nesting callback and visiting callback hell.
Promise chaining also allows you to pass the returned values down between the callbacks in a Promise chain. In the example of the Fetch request, it means sending a request to your server, waiting for a response, and then processing that response when it comes back to our client.
See the code block below to understand Promise syntax and how Promises operate. I’ve tried to use very verbose function names to give you a sense of what’s going on:
new Promise(function doThisFirst(resolve, reject) {
console.log('Hey, I just met you')
resolve()})
.then(function doThisSecond() {
console.log('and this is crazy')
}).then(function doThisThird() {
console.log('But here\'s my number')
}).then(function doThisFourth() {
console.log('so call me, maybe')
}).catch(() => {
console.error('Number engaged');
});
Second and in addition to my first point, because we can run what appears to the user as blocking tasks whilst still running our main thread of execution, we get the flexibility to execute other code. This means that the user experience is preserved as they can go about other tasks in the browser without being held up by the Promise logic.
In Chromium, this is achieved by the browser doing a tonne of juggling using the Event Loop to run our code in a concurrent-like way. To understand this in more detail, check out the great talk that Phillip Roberts did at JSConf in 2014, which explains how that works.
Third, Promises give you the ability to tie in error handling logic neatly. This is because of how Promises are executed. A Promise object chains functionality together to either be pending, resolved (fulfilled), or rejected. With the addition of the .catch
Promise syntax, you can have your Promise execute a console.log
of your error upon the failure of one of the functions called within the Promise chain. Conceptually similar JavaScript concepts are try-catch blocks. In the code block above we’d log “Number engaged” if for whatever reason the other chains in the Promise failed. Promise error handling helps with debugging since the error is contained in the block of works associated with the Promise workflow. This is particularly useful when you make network requests and troubleshoot why you weren’t served data from a given API endpoint.
Promise state flow
As a heads up, there is one main limitation you might come across when playing around with Promises. As I said at the start, they are JavaScript objects but special ones so you can’t simply manipulate them as you would an array or regular object literal. If you console.log()
out your Promises you’ll probably see what I mean. What is returned is illegible and unusable. Something like this:
Promise { <pending> }
Instead when working with Promises be sure to have the callback leverage closure to take whatever data is manipulated inside your Promise and update a relevant data type in the lexical scope of the parent execution context, thereby assuring that you can both get the most out of your Promises but not run into any usability bugs when coding.
Hopefully, by now, you will understand more about how JavaScript works under the hood than what you did at the start of the article and, in turn, how Promises can help you when you code. Of course, there’s much more to be said on the topic of Promises, but as a point of departure, we now know that they can give us the flexibility to write blocking asynchronous and graceful error handling code.