Asynchronous Functions
Promises are how JavaScript operates asynchronously, at its very core. What you’ve seen so far in this module aren’t the only syntaxes for working with asynchronous JavaScript, but it all comes back around to Promises eventually.
When you prefix a function declaration with the async keyword, that function will return a Promise every time it is called, no matter what the code inside does. If the code returns a value, then the caller will receive a Promise that is resolved to that value:
- Code language
- js
async function theFunction() { return "A string."; } console.log( theFunction() ); // Result: Promise { <state>: "fulfilled", <value>: "A string." }
Same goes for function expressions:
- Code language
- js
const theFunction = async function() { return "A string"; }; console.log( theFunction() ); // Result: Promise { <state>: "fulfilled", <value>: "A string" }
…Including arrow functions:
- Code language
- js
const theFunction = async () => "A string"; console.log( theFunction() ); // Result: Promise { <state>: "fulfilled", <value>: "A string" }
Just like an executor function when using the Promise constructor, a thrown error will be translated into a rejected Promise:
- Code language
- js
async function theFunction() { throw new Error( "Something went wrong." ); } console.log( theFunction() ); /* Result: Promise { <state>: "rejected", <reason>: Error } Uncaught (in promise) Error: Something went wrong. */
Asynchronous functions all result in Promises, which can then be consumed like any other thenable object:
- Code language
- js
const theFunction = async () => "A string"; theFunction().then( ( result ) => console.log( result ) ); // Result: A string
Out of the box, that may look like more or less the same result as the using the Promise constructor:
- Code language
- js
const thePromise = new Promise( ( resolve ) => { resolve( "A string" ); }); thePromise.then( ( result ) => console.log( result ) ); // Result: A string
Well, you’re right. Asynchronous functions provide you with a layer of all-too-familiar abstraction — they allow you to manage asynchronous operations using the same workflows as you already know and love from synchronous development. Same great taste as regular function definitions and invocations, now with more asynchronicity! “Less synchronicity?” Listen, I’m not a scientist.
Of course, we’re missing the “asynchronous operations” part of these snippets again, which necessarily makes things a little trickier. Let’s start by writing a function that returns a Promise, like any built-in asynchronous method would:
- Code language
- js
function theTimer() { return new Promise( ( resolve ) => { setTimeout( () => { resolve( "Resolved." ); }, 5000 ); }); };
Now we’ve got a function that returns a ready-to-consume Promise — but instead of tapping into it with then, we’ll use an async function to act on the resulting value of that Promise:
- Code language
- js
function theTimer() { return new Promise( ( resolve ) => { setTimeout( () => { resolve( "Resolved." ); }, 5000 ); }); }; async function theFunction() { const timerResult = theTimer(); }; console.log( theFunction() ); // Result (immediately): Promise { <state>: "fulfilled", <value>: undefined }
Et voila, it… did not work.
The execution of theFunction continued on to log the variable that we bound to the result of the call to theTimer, before that timer had expired and resolve was called — the result was a pending Promise with an undefined value.
Defining a function as async doesn’t mean it will operate in an asynchronous way — it just means that it can. There’s no way for JavaScript to just kinda know when it should be awaiting the results of an asynchronous operation. We do, though, and we can tell it as much using the keyword await.
await is used to create an await expression — a way to explicitly say “don’t continue executing this function until the thenable that follows is settled, then give me the resulting value of that Promise.” An await expression effectively unwraps the wrapped value of a Promise.
Let’s make one small change to the snippet of code above — add await before the call to theTimer:
- Code language
- js
function theTimer() { return new Promise( ( resolve ) => { setTimeout( () => { resolve( "Resolved." ); }, 5000 ); }); }; async function theFunction() { const timerResult = await theTimer(); console.log( timerResult ); }; theFunction(); // Result (after five seconds): Resolved.
Now we’re in business. The await expression pauses the execution of an asynchronous function while the associated Promise is pending — our function doesn’t continue on to the console.log. Once the Promise is settled, the result of the await expression is the value of the Promise, and execution continues on.
Keep in mind that await can only be used in functions defined with async, and with good reason: if we could hit the brakes this way in a synchronous function, it would mean blocking the main thread until such time should arrive — a performance disaster.
Concurrencypermalink
This does bring us to another important point, though: that async function will be paused until the Promise that follows await resolves, which may not always be desirable in an asynchronous function that consumes multiple Promises.
To demonstrate, let’s clock in for one last shift at the Promise factory:
- 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." ) } }); }; async function theFunction() { console.log( "Starting." ); const firstTimerResult = await thePromiseFactory( 5000 ); console.log( "Continuing." ); const secondTimerResult = await thePromiseFactory( 6000 ); console.log( firstTimerResult, secondTimerResult ); }; theFunction(); // Result (immediately): Starting. // Result (after five seconds): Continuing. // Result (after eleven seconds): 5000ms is up 6000ms is up
Everything happens in sequence: our function starts up, awaits the result of the first call to thePromiseFactory, continues on, awaits the results of the second call to thePromiseFactory, then concludes. That’s a strength when you’re writing an asynchronous function that makes use of multiple asynchronous operations that depend on each other — say, an initial call to fetch that pulls in information about a user, which contains a URL for that user’s avatar, then a second call to fetch to retrieve that avatar.
These timers are independent operations, though — the second doesn’t need the result of the first for anything, so there’s no reason to delay it until after the first has completed. In the case of the second operation, it breaks with the expectation that theFunction will complete 6000ms after being called — our second call to thePromiseFactory becomes an 11000ms timer. No good.
Ah, but we’re still working with Promises, which means we already have the tools for this use case and more: we can use the concurrency methods provided by the Promise constructor function. In this case, we want to log the results of both timers once they’ve all concluded, so Promise.all is the tool for the job. Better still, because all of those composition methods return arrays that contain the results of each Promise provided to them, we can use binding pattern destructuring to bind those results all at once:
- 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." ) } }); }; async function theFunction() { const [ firstTimerResult, secondTimerResult ] = await Promise.all([ thePromiseFactory( 5000 ), thePromiseFactory( 6000 ) ]); console.log( firstTimerResult, secondTimerResult ); }; theFunction(); // Result (after six seconds): 5000ms is up 6000ms is up
Break the the causal arrow of time across your knee — death to Chronos! Not only have we freed ourselves from the cruel marches of both linear time and the call stack, but they are now ours to weave into and out of at will.
Granted, as any seasoned time-traveler will, or did, or is telling you: restitching the fabric of time itself is tricky business. This is advanced stuff, and another case of a module where you shouldn’t expect to walk away from it with a complete and total understanding of the mechanics of asynchronous JavaScript. You’ll recognize a Promise from a mile away now, though, and you’ll know how to consume one even if it does mean the occasional MDN double-check for syntax specifics — that’s the nature of the job.
Besides, as I’m sure I’ll end up saying once I write it: consider this course to be a fixed point in time. You can revisit it from start-to-finish as often as you want, whenever you want, to brush up on the details at any point in your JavaScript journey — and there I’ll be, delighted to meet you for the first time all over again.