JavaScript Promises part 3: Wrapping up
31 Jul 2019
Welcome to part 3 and the final part of this guide to JavaScript promises. If you haven't read part 1 and part 2 yet, I strongly suggest you start there. I'll wait for you here!
In this article we'll be rounding off, looking at promise events, how the async/await
combo really made promises sing, and some of the promise-based APIs out there such as the Fetch API.
Promise events 🔗
The Promise API defines two events used to catch rejected promises. These live on the global scope (i.e. on window
or, if in a web worker, the Worker
object) These are:
rejectionhandled
- called after a rejected promise is caught by an error handler, e.g. by chaining acatch()
to a promiseunhandledrejection
- fires when a rejection is not caught by any error handler
unhandledrejection
shouldn't be used as a replacement for explicitly handling rejections. For one thing, the event doesn't prevent the error being sent to the console (sidenote: it does in Node.JS if you call event.preventDefault()
). They just provide a top-level means of being notified when a promise is rejected.
For both events, the event object passed to the callback contains two useful members: reason
, i.e. the rejection value, and promise
, the promise object.
Promise.reject('Oh this is bad!');
window.onunhandledrejection = evt => {
console.log(evt.reason); //"Oh this is bad!"
};
Promises + async/await 🔗
Promises are cool, but ECMAScript 2016's addition of the async/await
combo really makes them shine.
If you're unfamiliar with async/await
, you can read my guide to them. I won't duplicate all that here, so you may want to pause and go read that before continuing with this section.
async/await
is entirely based on promises, specifically:
- a function definition preceded with
async
ensures it implicitly returns a promise - the
await
keyword expects a promise as the value after it - anything else will be implicitly wrapped in a promise
Suddenly, we didn't even need then()
callbacks anymore. We could literally write ostensibly synchronous(-looking) code that hid away the promises and their asynchronicity as an implementation detail. This is because await pauses the current execution flow until the promise to the right of it resolves.
Recall our getData()
function from earlier, which returns a promise which resolves to either fetched (AJAX) or cached data. With async/await
we can use it like this:
(async () => { //have to be in an async closure
let data = await getData();
console.log(data); //our data!
let etc = await 15; //not a promise; one is created, and fulfilled with 15
console.log(etc); //15
})();
Because await pauses execution, this means we can even use traditional try-catch
blocks on our promises.
try {
let foo = await new Promise((resolve, reject) =>
setTimeout(() => reject('Oops!'), 1000)
);
} catch(e) {
console.log(e); //"Oops!"
}
For more on async/await
, check out my guide to them.
Promise-based APIs 🔗
async/await
isn't the only API based on promises - there's a few others. Perhaps the most notable is the Fetch API, which to a large extent replaces (or in the very least makes more elegant) the old, trusty XMLHttpRequest()
means of firing AJAX requests.
fetch()
returns a promise that resolves to the response of the request - whether it succeeds or not. Because it returns a promise, we can chain things to it like we've been doing so far.
Let's suppose we post some data to an endpoint and expect a JSON response.
fetch('/some/endpoint', {
method: 'post',
body: JSON.stringify({foo: 'bar'})
})
.then(response => response.json()) //same as response => { return response.json(); }
.then(data => console.log(data));
Or we could be really cool and combine the Fetch and Promise APIs with the async/await
combo we met a moment ago. (For brevity, I'll omit the async
closure this time.)
Another API that uses promises is the Media Devices API, which is used to gain access to a user's screen, webcam or microphone for rich-media applications.
Via this API we first have to request permission from the user to access a specific device. Since this is an asynchronous process (it'll take the usr some seconds to notice the prompt and click the allow or disallow button), promises are a perfect fit.
Let's ask for access to our user's microphone.
navigator.mediaDevices.getUserMedia({audio: true})
.then(stream => { /* use the stream */ })
.then(err => { /* an error, or permission was refused */ });
Micellaneous 🔗
So now we've covered all the main parts of the Promise API. I'd like to end on a couple of minor points which are not crucial to understanding promises but which I include for completeness.
Firstly, be aware that JavaScript will never call promise callbacks until the end of the current run. This means JavaScript will finish executing the synchronous code flow of the current scope before our callbacks fire.
In this way, promises are always asynchronous - even if they ostensibly do synchronous work.
This differs from usual JavaScript chaining, where the operations are synchronous. Consider jQuery chaining:
Secondly, you may recall from part 1 that promises can end up in fulfilled or rejected states, and that both constitute a resolved fate. With this in mind, why do we have Promise.resolve()
, not Promise.fulfil()
?
It comes down to the fact that, as it turns out, a resolve callback (or Promise.resolve()
) is USUALLY used to fulfil a promise but not always; it can also be used to reject a promise (whereas a rejection callback, or Promise.reject()
, is always used to reject).
Admittedly, this is where things get a little edge-case, and a bit mind-bending. But the takeaway here is: we can resolve a promise with a rejected promise.
Say what? Let's take a look.
Promise.resolve(Promise.reject('No!'))
.then(value => console.log(value)) //never gets called
.catch(reason => console.log(reason)); //"No!"
So although we resolved our outer promise - in a way that normally fulfils it - we actually rejected it because we resolve it with a rejected promise. Have a read of Kesk Noran's article on this if you're interested.
Summary 🔗
Promise events can be used to catch times when rejected promises are, or are not, caught by error handlers.
The async/await
combo really makes promises shine because await
pauses code execution until the promise resolves, meaning we no longer need then()
callbacks. Our code looks more synchronous than ever, despite performing asynchronous operations.
Other standard APIs use or depend on promises, such as the Fetch API, which is a cleaner, more elegant means of performing AJAX requests, with each request returning a promise.
And that's it! I hope you've enjoyed this detailed look at promises and found it useful.
Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!