Consuming Promises
I bet I end up saying this all the time, but we’re coming up on another one of my favorite syntaxes. I mean, I know me; how much time do I spend gushing about ?. once I get around to it? Ah, no, right, spoilers — nevermind.
The then() method provided by a Promise object’s prototype chain isn’t the favorite syntax, but it is part of it, so stay tuned. then() allows you to specify code to be executed when that Promise is settled. It accepts two callback functions as arguments. The first is the function to be invoked if and when the Promise resolves, and the second is the function to be invoked if and when a Promise is rejected:
- Code language
- js
const theExecutor = ( resolve, reject ) => { // Resolve after five seconds: setTimeout(() => { resolve( "Time's up." ); }, 5000); }; const thePromise = new Promise( theExecutor ); thePromise.then( ( result ) => { console.log( result ); }); /* Result (once the operation completes): Time's up. */
I should ask the Piccalilli team to wire up an airhorn.mp3 to play once you scroll to certain points in these lessons. This is a big reveal! You now have code that executes if and when a Promise has resolved; code freed from the ceaseless march of the call stack, and— well, okay, yes, that code still ends up in the call stack, as all JavaScript must, but asynchronously, thanks to the event loop.
Editors note: Mat, we’ve talked about this.
For a predictable rejection, let’s try pinging my website again:
- Code language
- js
fetch( "https://wil.to", {}).then( function( result ) { console.log( result ); }, function( error ) { console.log( "That didn't work:" ); console.error( error ); }); // Result: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at <https://wil.to/>. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200. /* Result (once the operation completes): That didn't work: TypeError: NetworkError when attempting to fetch resource. *
The code we specify in the resolve function is never executed, because the Promise didn’t resolve. The code we specified in the reject function does, though — and the argument represents the specifics of the error. We can do whatever we want with that information, but logging it as an error is as sensible a move here as any, I figure.
You’ll notice that the “Cross-Origin Request Blocked…” error shows up unbidden — that’s a browser error, not a JavaScript-specific one. A TypeError is specific to JavaScript, and that’s the value caught by the reject function. To make that line a little clearer, we’ll do the same thing with a five-second delay built into our reject function:
- Code language
- js
fetch( "https://wil.to", {}).then( function( result ) { console.log( result ); }, function( error ) { setTimeout( () => { console.error( error ); }, 5000 ); }); // Result: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at <https://wil.to/>. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200. /* Result (five seconds after the operation completes): TypeError: NetworkError when attempting to fetch resource. */
The browser error shows up right away to notify you that this request is a non-starter, and because fetch has resulted in an error, our reject function is executed. After five seconds, we send the result of the fetch operation to the console as an error: now our TypeError shows up.
Now, I don’t love this exact use of then(), I don’t mind saying. No matter how you format it, supplying the functions as arguments directly can feel a little overloaded:
- Code language
- js
const theExecutor = ( resolve, reject ) => { setTimeout(() => { resolve( "Time's up." ); }, 5000); }; const thePromise = new Promise( theExecutor ); thePromise.then( function( result ) { //... }, function( error ) { // ... } ); thePromise.then( result => { //... }, error => { // ... } );
Defining one-time use functions just for the sake of readability feels wordy, too:
- Code language
- js
const theExecutor = ( resolve, reject ) => { setTimeout(() => { resolve( "Time's up." ); }, 5000); }; const thePromise = new Promise( theExecutor ); const resolved = ( result ) => { // ... }; const rejected = ( result ) => { // ... }; thePromise.then( resolved, rejected );
So now we arrive at one of my favorite syntaxes.
then() is one of three methods inherited from the Promise constructor for interacting with a single Promise: then() is invoked when a Promise is settled, whether resolved or rejected. catch() is only invoked if and when a promise is rejected. finally(), like then(), is also invoked once a Promise is settled, regardless of outcome — though finally() doesn’t recieve a value from the Promise. The use case for finally() will become clear in just a moment, as we once again queue up the airhorn.mp3 that I specifically requested earlier, Andy, and we learn about the best part of working with Promises: Promise chaining.
then(),catch(), and finally() can all return either a Promise or a Promise-like object called a thenable — an object that implements a then() method. That allows us to write what is — to my mind — some of the most “self-documenting” code JavaScript has to offer:
- Code language
- js
const theExecutor = ( resolve, reject ) => { setTimeout(() => { resolve( "Time's up." ); }, 5000); }; const thePromise = new Promise( theExecutor ); thePromise .then( result => { return `The result was: "${ result }"`; } ) .then( result => { console.log( result ); } ); // Result (after five seconds): The result was: "Time's up."
Any non-Promise value returned by the function supplied to then() will be wrapped in a Promise-like object called a thenable. A thenable is an object that implements a then() method like a Promise does, allowing you to continue acting on it as you would a Promise. That means we can then call then() on that result to take action based on its result. Do this, then this, then this.
The Promise returned by then() will track the status of the promise then() was invoked on. This allows you to chain multiple asynchronous operations together and ensure that they’re always executed in sequence:
- Code language
- js
const theExecutor = ( resolve, reject ) => { setTimeout(() => { resolve( "Time's up" ); }, 5000); }; const thePromise = new Promise( theExecutor ); thePromise .then( result => { return new Promise( ( resolve, reject ) => { console.log( "Actually, five more seconds." ); setTimeout(() => { resolve( `${ result }, five seconds later.` ); }, 5000); }); }) .then( result => { console.log( result ); }) .catch( error => { console.log( "You won't see this unless something went wrong."); } ); // Result (after five seconds): Actually, five more seconds. // Result (after five more seconds): Time's up, five seconds later.
If there’s an exception thrown anywhere in the Promise chain — or we explicitly call the reject function in our executor — the browser will look down the chain for catch():
- Code language
- js
const theExecutor = ( resolve, reject ) => { setTimeout(() => { resolve( "Time's up." ); }, 5000); }; const thePromise = new Promise( theExecutor ); thePromise .then( result => { throw new Error( "Something went wrong!" ); return `The result was: "${ result }."`; }) .then( result => { console.log( result ); }) .catch( error => { console.log( "You won't see this unless something went wrong."); } ); // Result (after five seconds): You won't see this unless something went wrong.
catch() and finally() both call then() internally — they’re not really doing anything then() can’t. Invoking catch() is equivalent to invoking then() without passing it a resolve function.
There is a fairly major difference with finally(), though: it doesn’t consume nor result in a thenable. It isn’t for use as a step in the Promise chain. Just like the name implies, it’s used to perform tasks related to the completion of a Promise chain — for example, closing a file or network connection:
- Code language
- js
(function() { const airhornPromise = new Audio( "https://assets.codepen.io/11355/airhorn.mp3" ); airhornPromise.play() .catch( error => { console.log( error ); }) .finally( () => console.warn( "WAH-WAH-WAAAH" ) ); })();
Andy, where are we at on implementing this, by the way? Listen, I’ll ping you in Slack. Seriously, I’m not moving on to the next section until we figure out this airhorn th
Concurrencypermalink
Fine, fine.
The Promise constructor also provides you with methods for working with multiple related Promises all at once, by way of an array of Promise objects. These methods all return a thenable that is either fulfilled or rejected based on the state of the Promises passed to it.
That’s tricky in prose, I know, but another choice syntax for legibility. For example, Promise.all() creates a Promise that is fulfilled only if and when all Promises passed to that method are fulfilled, and provides you with an array of those Promises’ results:
- Code language
- js
const thePromiseFactory = function( theTimer ) { return new Promise( ( resolve, reject ) => { setTimeout(() => { resolve( `${ theTimer }ms is up` ); }, theTimer ); }); }; const shortPromise = thePromiseFactory( 5000 ); const longPromise = thePromiseFactory( 10000 ); Promise .all([ shortPromise, longPromise ]) .then( result => { console.log( result ); console.log( "All promises have concluded." ); }); /* Result (after ten seconds): Array [ "5000ms is up", "10000ms is up" ] All promises have concluded.
If any of the promises in the array rejects, Promise.all() immediately rejects the returned promise — they didn’t all resolve:
- Code language
- js
const thePromiseFactory = function( theTimer ) { return new Promise( ( resolve, reject ) => { if( typeof theTimer === "number" ) { setTimeout(() => { resolve( `${ theTimer }ms is up` ); }, theTimer ); } else { throw new Error( "Use numbers for setTimeout delays." ) } }); }; const shortPromise = thePromiseFactory( 5000 ); // This one won't work: const longPromise = thePromiseFactory(); Promise .all([ shortPromise, longPromise ]) .then( result => { console.log( result ); console.log( "All Promises have concluded." ); }, error => { console.warn( error ); } ); // Result: Error: Use numbers for setTimeout delays.
Promise.allSettled() fulfills when all promises settle, regardless of whether all the Promises supplied to it fulfill or reject. Promise.any() fulfills when any Promise provided to it fulfills, and rejects if and when none of the promises supplied to it fulfills.
Promise.race() settles as soon as any of the Promises supplied to it settle — it fulfills if the first Promise across the finish line fulfills and rejects if it rejects:
- Code language
- js
const thePromiseFactory = function( theTimer ) { return new Promise( ( resolve, reject ) => { setTimeout(() => { resolve( `${ theTimer }ms` ); }, theTimer ); }); }; const shortPromise = thePromiseFactory( 2000 ); // This one won't work: const longPromise = thePromiseFactory( 5000 ); Promise .race([ shortPromise, longPromise ]) .then( result => { console.log( "The first Promise has concluded." ); console.log( `It was the one that settled after ${ result }` ); }); /* Result (after two seconds): The first Promise has concluded. It was the one that settled after 2000ms */
I like a “just like it says on the label” syntax, and Promise chaining and composition are all-timers in that arena for sure. Still though, working with Promises isn’t the most approachable thing in the world, all things considered; I like chaining, but it is a different development paradigm than you’ll encounter throughout the majority of the language.
To account for that, with ES2017 came two keywords that allow you to make use of what should be — assuming I did my job, here — much more familiar syntaxes when working with Promises: async and await.