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:
let p = ... // some expression producing a promisethe promise is in the pending state. Calling:
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 then
s, and why after a successful catch
, we can resume the chain of then
s.
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.