Demystifying Promises in JavaScript: How Do They Work? - by Michael E Colley - Nov, 2021 - Better Programming
Demystifying Promises in JavaScript: How Do They Work? - by Michael E Colley - Nov, 2021 - Better Programming
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?
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.
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.
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.
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:
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.