JavaScript programs are single-threaded: program execution consists of running uninterruptible code blocks in response to events. All code blocks are run to completion; we never run two or more code blocks together at the same time. (A good JavaScript engine will know how to use multiple processor cores but will make the code work the same as if only one thread existed.)
This is probably the simplest async program:
console.log("First") setTimeout(() => {console.log("Third")}, 0) console.log("Second")
This program reliably and deterministically prints First then Second then Third.
setTimeout(f, t) is just a scheduling call. It says ”after at least t milliseconds has passed, and after the current “block” of code is done, run function f. So here, it is only after the script prints First and Second that the delayed function can be run to print Third.
Always remember: JavaScript code never gets interrupted. If we had a long running script that makes a call to setTimeout(f, 50), the scheduling call will return immediately while the script keeps going until completion. The script will not be interrupted in 50 milliseconds. Even if the script takes 10,000 milliseconds to complete, the scheduled function will just have to wait. (In other words, 50 means “at least 50”.)
There are a few different ways to deal with things that are asynchronous.
Let’s start with an example. In Node’s fs module there is a readFile function, defined by:
function readFile(filename, options, processTheFile) { // Schedules a block of code to run. The scheduled code will read the file // then will invoke processTheFile(...), passing it the file data that was // read, and/or error information. }
and called like this (using the simplest possible boring example):
// Here is how we call the async function. We defined the callback as an // anonymous function thought that wasn’t strictly necessary. The first // argument to the callback is an error object if reading the file failed // or null otherwise. The second argument holds the lines of the file that // was successfully read. The body of a callback almost always begins with // an error check. fs.readFile('stuff.txt', 'utf-8', (error, data) => { if (error) throw error console.log(data) // (for example) })
Because reading a file can take time, JavaScript makes it async: The call to readFile only schedules the code to read this file and therefore returns almost immediately. So readFile doesn’t actually read the file. This simple example:
thinkAboutPoems() readFile('BecauseICouldNotStopForDeath', recitePoem) eatLunch()
actually invokes eatLunch() before the poem gets recited! Only after the file is read, and after the whole script is done (including the eating of lunch) will we finally invoke recitePoem.
So this is pretty cool: because readFile is an async function, we get to eat lunch without waiting for the file to be read! With asynchronous programming we almost never have to “wait.” This often helps the processor stay busy and helps gives such systems higher throughput.
Sometimes you have to do a number of asynchronous operations one after the other. What does this look like?
function runJob(configFile, outputFile) { initializeConfiguration(configFile, (err, configInfo) => { if (err) throw err fetchData(configInfo.whatever, (err, rawData) => { if (err) throw err processData(rawData, (err, processedData) => { if (err) throw err analyze(processedData, (err, analysis) => { if (err) throw err report(analysis, outputFile, (err) => { if (err) throw err console.log(`Report written to ${outputFile}.`) }) }) }) }) }) }
Do you find that code pretty? Many people don’t. Nesting callbacks like this is called callback hell.
One way to avoid callback hell is probably to just make the callbacks explicit:
function runJob(configFile, outputFile) { initializeConfiguration(configFile, handleConfiguration) function handleConfiguration(err, configInfo) { if (err) throw err fetchData(configInfo.whatever, handleFetch) } function handleFetch(err, rawData) { if (err) throw err processData(rawData, handleProcessing) } function handleProcessing(err, processedData) { if (err) throw err analyze(processedData, handleAnalysis) } function handleAnalysis(err, analysis) { if (err) throw err report(analysis, outputFile, handleReport) } function handleReport(err) { if (err) throw err console.log(`Report written to ${outputFile}.`) } }
Did that really help?Meh, probably not. There must be a better way! And there is!
But... make sure you understand the concept of a callback before looking at more advanced features.
Instead of writing an async function that accepts a callback as a parameter, your function can return a Promise to which you apply callbacks. In the simplest case, instead of the classic:
myFunction(args, callback)
you write:
myFunction(args).then(callback)
At first glance, that doesn’t seem like much, but promises have one huge plus in their favor: they are composable, so when you have lots of async operations, things are going to look much cleaner. Examples first and explanations later. Remember the example from before? Here is how we would write it if we change each of the async functions to return promises:
function runJob(configFile, outputFile) { initializeConfiguration(configFile) .then(configInfo => fetchData(configInfo.whatever)) .then(rawData => processData(rawData)) .then(processedData => analyze(processedData)) .then(analysis => report(analysis, outputFile)) .then(() => console.log(`Report written to ${outputFile}.`)) .catch(err => { throw err; }) }
In this code above, initializeConfiguration schedules some async work and returns a Promise object. Promise objects have two methods, then and catch, which attach callbacks to the promise. The cool thing is calling then just returns another promise. So the chaining we end up with is pretty sweet.
Okay, so what exactly is a promise? How do you make one? How do they work internally?
A promise is an object representing a value which may or may not be ready yet, or may never be ready. It may be in one of three states:
Technical time! At the most basic level if we have:
the promise is in the pending state. Calling:let p = ... // some expression producing a promise
p.then(workOnIt, fixIt)
means (roughly): “if / when the promise p gets fulfilled, workOnIt (if it is a function) gets called; and if / when p gets rejected, fixIt (if it is a function) gets called.”
Note that:
p.then(f) means the same as p.then(f, undefined)
p.catch(f) means the same as p.then(undefined, f)
But what’s cool is that p.then returns another promise, so we can write nice chains.
What happens if the promise is already fulfilled or rejected before a handler gets attached? (Good question if you thought to ask it!) Well don’t worry: the handler will get called right away.
Most programmers never create their own promisesMost likely you will be using, not creating, promises, so feel free to skip this section on a first read.
The promise constructor takes in a single argument (known as the “executor”), which is a function of two arguments, generally called resolve and reject. The executor body should call resolve on success and reject on failure:
const promise = new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() < 0.5) resolve("Yes") else reject("No") }, 250) }) promise .then(message => { console.log(`ok: ${message}`); }) .then(() => { console.log ('Who knew?'); }) .catch(message => { console.log(`fail: ${message}`); }) .then(() => { console.log('Done'); });
Run this code multiple times to see different outputs.
Note you never have to call the executor function directly, ever. The Promise constructor calls it for you, passing internal functions (for resolving and rejecting) to your executor.
In practice, you will write your own async functions using a pattern something like this:
function myFunction(args) { return new Promise((resolve, reject) => { // ... }); }
So remember, async functions either (1) accept your callback or (2) return a promise.
Lets say we have:
p1.then(f1, g1).then(f2, g2).then(f3, g3)
which we will rewrite to the completely equivalent:
p2 = p1.then(f1, g1); p3 = p2.then(f2, g2); p4 = p3.then(f3, g3);
Now the way promises are set up to work is:
if p1 is fulfilled
if f1 is a function
if f1() succeeds
fulfill p2
else
reject p2
else
reject p2
if p1 is rejected
if g1 is a function
if g1() succeeds
fulfill p2
else
reject p2
This is why rejections can propagate through a series of simple thens, and why after a successful catch, we can resume the chain of thens.
You can do other cool things:
Promise.all([p1, p2, p3, ..., pn])
Promise.allSettled([p1, p2, p3, ..., pn])Promise.race([p1, p2, p3, ..., pn])
Promise.resolve(value)
Promise.reject(reason)
Gratuitous examples:
> Promise.resolve(100).then(x => {console.log(x);})
Promise { <pending> }
100
> Promise.resolve('dog').catch(x => {console.log(x);})
Promise { <pending> }
> Promise.reject('dog').catch(x => {console.log(x);})
Promise { <pending> }
dog
You are most likely to encounter promises when working with files, network connections, databases: things that take a while to access or process. On the web, you are likely to use fetch: in its simplest form it accepts a url and fetches the data on the web at that url. fetch returns a promise that resolves to the server’s response. (Why a promise? Because you don’t know how long it will take to get that response!)
For many resources on the web, the body of the response is in JSON format, which you can obtain via response.json() but you guessed, it, it takes time to parse that response body so response.json() returns a promise. So let’s chain! Here’s how to use fetch to get an image of a Pokémon from the PokéAPI:
function showPokemon(e) { fetch("https://pokeapi.co/api/v2/pokemon/pikachu").then(response => { if (response.status >= 200 && response.status < 400) { response.json().then(pokemon => { picture.src = pokemon.sprites.front_default error.textContent = '' } } else { picture.src = '' error.textContent = `Problem, server said ${response.status}` } }) }
Try it:
Modern JavaScript has two new keywords that make writing async code even more elegant.
So remember from above how an async function returns a promise? Well you can actually mark a function with the keyword async, which means:
Promise.resolve.
Promise.reject.
> async function div(x, y) {if (y === 0) throw 'divBy0'; else return x/y;}
undefined
> div.constructor
[Function: AsyncFunction]
> p = div(35, 7)
Promise { 5 }
> function show(x) { console.log(x); }
undefined
> p.then(show)
Promise { <pending> }
5
> div(5, 0).catch(show)
Promise { <pending> }
divBy0
An async function can contain an await expression, where await p suspends execution of the function until promise p is settled (i.e., fulfilled or rejected) and then resumes. This is NOT synchronous blocking! Other code can run during the time the async function is suspended, just like in a JavaScript generator.
If p is fulfilled, the resolved value is produced, otherwise the await expression will throw the rejected value. So convenient.
In the running example, assuming the utility functions are all marked async, we can just write:
async function runJob(configFile, outputFile) { const configInfo = await initializeConfiguration(configFile); const rawData = await fetchData(configInfo.whatever); const processedData = await processData(rawData); const analysis = await analyze(processedData); await report(analysis, outputFile); console.log(`Report written to ${outputFile}.`); // No need to catch and rethrow anything, because all the awaits will throw if necessary! }
In the Poké API example, with async-await we only need:
async function showPokemon(e) { const response = await fetch("https://pokeapi.co/api/v2/pokemon/pikachu") if (response.status >= 200 && response.status < 400) { const pokemon = await response.json() picture.src = pokemon.sprites.front_default error.textContent = '' } else { picture.src = '' error.textContent = `Problem, server said ${response.status}` } }
Try it:
Well, pretty much everything we saw above is explained by Wassim Chegham’s brilliant nine-second explanation:
@maxlath @naholyr @maxdow Here is a special extended 9 seconds animation just for you 😛 pic.twitter.com/ZpQ3bD1DtE
— Wassim Chegham (@manekinekko) April 22, 2017
Sweet, right? Okay now go write correct, robust, async code of your own.