Introducing async JavaScript
Synchronous JavaScript
To understand what asynchronous code is, we need to understand what synchronous code is. In this block of code,
the lines are executed one after the other:
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
alert('You clicked me!');
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph';
document.body.appendChild(pElem);
});
While each operation is being processed, nothing else will happen (rendering is paused). This is because
client-side JavaScript is single-threaded blocking language: only one thing can happen at once, on a single main
thread. Everything else is blocked until the current operation completes.
Asynchronous JavaScript
To avoid such blocking, many Web APIs now use asynchronous code, especially for fetching resources from an
external device (accessing a database and returning data from it, for instance). There are two main types of
asynchronous code:
- old-style callbacks
- newer-style promises
Async callbacks
Callbacks are functions that are passed as parameters to other functions to be executed whena previous
operation has returned. The second parameter of addEventListener()
is an example:
btn.addEventListener('click', () => console.log('I am the callback!'));
The first paramater is the type of event to be listened for, and the second parameter is the function that is
invoked when the event is fired.
When we pass a callback function to another function, we only pass the function definition as the parameter, so
that this callback function is not executed immediately. It is "called back" (hence the name) asynchronously,
somewhere inside the containing function's body. The containing function is responsible for executing the
callback function when the time comes.
function loadAsset(url, type, callback) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = type;
xhr.onload = () => callback(xhr.response);
xhr.send();
}
function displayImage(blob) {
//displaying the resource on the user interface
}
loadAsset('../images/coffee.jpg', 'blob', displayImage);
Here the displayImage()
function creates an image from the url's target and appends it to the
document. The resource is fetched by the loadAsset()
function and it passes the response to the
callback to do something with it.
Callbacks are versatile, they allow you to control the order in which functions are run, but also how and what
data is passed between them. Note that some callbacks run synchronously, like when
Array.prototype.forEach()
loops through the items of an array.
const gods = ['Apollo', 'Artemis', 'Hermes', 'Zeus', 'Helios'];
gods.forEach((godName, index) => {
console.log(`${index} - ${godName}`);
});
In this case, forEach()
takes a unique callback as parameter and it runs immediately, without
waiting for
anything.
Promises
Promises are the new style of async code, used in modern web APIs. The fetch()
API is a good
example. It is basically a more modern and efficient version of XMLHttpRequest
:
fetch('../source/products.json')
.then((response) => response.json())
.then((json) => {
products = json;
initialize();
})
.catch((error) => console.log(`Fetch problem: ${error.message}`));
fetch()
takes a single parameter, the url of the resource to fetch from the network, and returns a
promise. The promise is an object representing the result (completion or failure) of the async operation. It
represents an intermediate state. In essence, it's the browser's way of saying: "I promise to get back to you
with the answer as soon as I can". Then we've got three further code blocks chained onto the end of the
fetch()
operation:
- Two
then()
blocks. Both contain a callback function that will run if the fetch operation is
successful. Each callback receives the result returned by the previous successful operation. Each
then()
block returns a promise, meaning you can chain multiple then()
blocks onto
each other, so multiple async operations can be made to run in order.
- The
catch()
block, at the end, runs if any of the previous then()
blocks fails. It
is similar to try {} catch {}
, an error object is made available inside which can be used to
report the kind of error that has ocurred.
Async operations like promises are put into an event queue, which runs after the main thread has finished
processing and so does not block subsequent JavaScript code from running. The queued operations
will complete as soon as possible then return their results to the JavaScript environment.
Conclusion
At its most basic, JavaScript is a synchronous, blocking, single-threaded language: only one action can be in
progress at a time. But web browsers define functions and APIs that allow us to register functions that should
not be executed synchronously but when some kind of event occurs (passage of time, user's interaction with the
mouse, arrival of data from the network, etc.). It means that you can let your code do several things at the
same time without blocking the main thread.
Async loops and intervals
For a long time, JavaScript has made available a number of functions that allow you to asynchronously execute
code after a certain time interval has elapsed, and repeatedly execute a block of code periodically until you
tell it to stop. These are:
setTimeout()
— Execute a specified block of code once after a specified time has elapsed.
setInterval()
— Execute a specified block of code repeatedly with a fixed time delay
between each call.
requestAnimationFrame()
— The modern version of setInterval()
, it executes a
specified block of code before the browser next repaints the display, allowing an animation to be run at a
suitable framerate, regardless of the environment it is being run in.
These functions actually run on the main thread, but you're able to run other code between iterations to a more
or less efficient degree, depending on how processor intensive these operations are. These functions are used
for running constant animations and other background processing on a web site or application.
setTimeout()
setTimeout()
executes a specified block of code once after a specified time has elapsed. It takes
the following parameters:
- A function to run, or a reference to a function defined elsewhere.
- A number representing the number of milliseconds to wait before executing that function. If you specify a
value or 0 or omit the number, then the function will run immediately.
- Zero or more values that represent the parameters you want to pass to the function when it is run.
Here, the browser will wait for 2 seconds and display an alert message:
const myGreeting = setTimeout(() => {
alert('Hello Mr Universe!');
}, 2000);
The function could be named and even defined somewhere else:
function sayHi() {
alert('Hello Mr Universe!');
}
const myGreeting = setTimeout(sayHi, 2000);
setTimeout()
returns an identifier value that can be used to refer to the timeout later, to stop
it for instance (see "Clearing timeouts", below).
Passing parameters to a setTimeout() function
Any parameter we want to pass to the function being run inside setTimeout()
have to be passed as
additional parameters, at the end of the list:
function sayHi(who) {
alert(`Hello ${who}!`);
}
let myGreeting = setTimeout(sayHi, 2000, 'Mr Universe');
The name of the person to say hello is passed as a third parameter.
Clearing timeouts
Finally, if a timeout has been created, you can cancel it before the specified time has elapsed by calling
clearTimeout()
and passing it the identifier of the setTimeout()
call as a parameter:
clearTimeout(myGreeting);
setInterval()
setInterval()
runs a block of code over and over again. It works in a similar way to
setTimeout()
, except that the function passed to it as first parameter is executed repeatedly at an
interval equal to the number of milliseconds provided as the second parameter. You can also pass any parameters
required by the function being executed in as subsequent parameters of the setInterval()
call.
The following function creates a new Date()
object, extracts a time string out of it, using
toLocaleTimeString()
and then displays it in the UI. We then run it once per second using
setInterval()
, creating the effect of a digital clock that updates once per second.
function displayTime() {
let date = new Date();
let time = date.toLocaleTimeString();
document.getElementById('demo').textContent = time;
}
const createClock = setInterval(displayTime, 1000);
Clearing intervals
setInterval()
keeps running a task forever. We may want to stop such tasks to prevent errors when
the browser can't complete a cycle or of the animation being handled has finished. We can do it the same way as
we stopped timeouts, by passing the identifier returned by setInterval()
to the
clearInterval()
function:
const myInterval = setInterval(myFunction, 2000);
clearInterval(myInterval);
Things to keep in mind about setTimeout() and setInterval()
Recursive timeouts
setTimeout()
can be called recursively to run the same code repeatedly, instead of using
setInterval()
. Here is an illustration:
let i = 1;
setTimeout(function run() => {
console.log(i);
i++;
setTimeout(run, 100);
}, 100);
Here is the version using setInterval()
:
let i = 1;
setInterval(() => {
console.log(i);
i++;
}, 100);
The difference between the two versions of the code is a subtle one
- Recursive
setTimeout()
guarantees a 100 ms delay between the executions. The code will run and
then wait 100 milliseconds before it runs again. The interval will be the same regardless of how long the code
takes to run.
setInterval()
does things somewhat differently. The interval we choose includes the
time taken to execute the code we want to run in. Let's say the code takes 40 milliseconds to run, then the
interval ends up being only 60 milliseconds.
When your code has the potential to take longer to run than the time interval you've assigned, it's better to
use recursive setTimeout()
. This will keep the time interval constant between executions regardless
of how long the code takes to execute, and you won't get errors.
Immediate timeouts
Using 0 as the value of setTimeout()
schedules the execution of the passed function as soon as
possible, but only after the main thread has been run. In the following code, the alert "World" will only run
after clicking OK on the alert "Hello":
setTimeout(() => alert('World'), 0);
alert('Hello');
This can be useful in cases where you want to set a block of code to run as soon as all of the main thread has
finished running. Put it on the async event loop, so it will run straight afterward.
Clearing with clearTimeout() or clearInterval()
clearInterval()
and clearTimeout()
use the same list of entries to clear from.
Interestingly enough, this means that you can use either method to clear a setTimeout()
or
setInterval()
. But to avoid confusion and for consistency, use the appropriate method.
requestAnimationFrame()
requestAnimationFrame()
is a specialized looping function created for running animations in the
browser. It is basically the modern version of setInterval()
. It executes a specified block of code
before the browser next repaints the display, allowing an animation to be run at a suitable framerate regardless
of the environment it is being run in.
The method takes a callback to be invoked before the repaint as argument. This is the general pattern you'll
see it used:
function draw() {
// Drawing code goes here
requestAnimationFrame(draw);
}
draw();
The idea is that you define a function in which your animation is updated, then you call it to start the
process off. At the end of the function block, you call requestAnimationFrame()
with the function
reference passed as the parameter and this instructs the browser to call the function again on the next display
repaint. This is then run continuously, as we are calling requestAnimationFrame()
recursively.
We don't specify a time interval for requestAnimationFrame()
. It just runs as fast and smoothly as
possible in the current conditions (as close as possible to 60 fps - frames per second). The browser doesn't
waste time running the animation if it is offscreen, etc.
Including a timestamp
The callback passed to the requestAnimationFrame()
function also accepts a parameter: a timestamp
value that represents the time since the animation started running. This is useful as this allows to run things
at specific times and at a constant pace, regardless of how fast or slow the device might be.
let startTime = null;
function draw(timestamp) {
if (!startTime) {
startTime = timestamp;
}
currenTime = timestamp - startTime;
// Do something based on current time
requestAnimationFrame(draw);
}
Clearing a requestAnimationFrame()
call
Clearing a requestAnimationFrame()
can be done calling the corresponding
cancelAnimationFrame()
, passing it the value returned by the requestAnimationFrame()
(stored in a variable)
cancelAnimationFrame(myRaF);
Async operations with Promises
A promise is an object that represents an intermediate state of an operation: a promise that a
result of some kind will be returned at some point in the future.
The trouble with callbacks
To fully understand why promises are a good thing, it helps to think back why old-style callbacks are
problematic.
Let's talk about ordering pizza as an analogy. There are certain steps that you have to take for your order
to be successful, which don't really make sense to try to execute out of order, or in order but before each
previous step has quite finished:
- You choose what pizza you want. This can take a while if you're indecisive and may fail if you can't make
up your mind, or decide to get a curry instead.
- You then place your order. This can take a while to return a pizza and may fail if the restaurant does not
have the required ingredients to cook it.
- You then collect your pizza and eat. This may fail if, says, you forgot your wallet so can't pay for the
pizza!
With old-style callbacks, a pseudo-code representation of the above functionality might look something like
this:
choosePizza((order) => {
placeOrder(order, (pizza) => {
collectOrder(pizza, (enjoy) => {
eatPizza(enjoy);
}, failureCallback);
}, failureCallback);
}, failureCallback);
This is messy and hard to read (often referred to as "callback hell"). If this were real code, it would
likely block the main thread until it completes and would require you to call the
failureCallback()
multiple times.
Improvements with promises
Promises make situation like the above much easier to write, parse and run. If we represented the above
peudo-code using asynchronous promises instead, we'd end up with something like this:
choosePizza()
.then(order => placeOrder(order))
.then(pizza => collectOrder(pizza))
.then(enjoy => eatPizza(enjoy))
.catch(failureCallback);
This is much better. It is easier to see what's going on and a single .catch()
block is needed
to handle all the errors. It doesn't block the main thread and each operation is guaranteed to wait for
previous operations to complete before running. We're able to chain multiple asynchronous actions to occur one
after another because each .then()
block returns a new promise that resolves when the
.then()
block is done running.
At their most basic promises are similar to event listeners, but with a few differences:
- A promise can only succeed or fail once. It cannot succeed or fail twice and it cannot switch from success
to failure or vice versa once the operation has completed.
- If a promise has succeeded or failed and you later add a success / failure callback, the correct callback
will be called, even though the event took place earlier
Explaining basic promise syntax: a real example
Promises are important to understand because most modern Web APIs use them for returning values. Later on,
we'll look at how to write your own promise, bu for now we'll look at some simple examples.
In the first example, we'll use the fetch()
method to fetch an image from the web, the
blob()
to transform the fetch response's raw body contents into a Blob
object, and
then display that blob inside and <img>
element. This is very similar to the very first
example we looked at in this page.
let promise = fetch('coffee.jpg');
This calls the fetch()
method, passing it the url of the image to fetch from the network as a
parameter. We are storing the promise object returned by fetch()
inside a variable. This object
represents an intermediate state that is initially neither success or failure. When a promise has not
completed yet, we say it is pending
To respond to the successful completion of the operation, whenever that occurs (when a Response
is returned in that case), we invoke the then()
method of the promise object. The callback inside
the then()
block (referred to as the executor) runs only when the promise call
completes successfully (when it is fulfilled, or resolved). It is passed the
Response
object as parameter. Note the then()
doesn't run until an event occurs
(when the promise fulfills). We immediately run the blob()
method on this response to ensure that
the response body is fully downloaded and then transform it into a blob object we can use.
let promise2 = promise.then(response => response.blob());
Each call to then()
creates a new promise and the call to blob()
also returns a
promise. We can handle the Blob
object stored in the second variable by invoking the
then()
method of the second promise.
let promise3 = promise2.then((myBlob) => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
Here we are running the URL.createObjectURL()
method. It is passed the Blob
object
returned by the second promise fulfillement, as a paramater. This will return a URL pointing to the object.
Then we create an <img>
element, set its src
attribute to equal the object URL
and append it to the DOM, so the image is displayed on the page.
Responding to failure
There is currently nothing to explicitly handle errors if one of the promises fails
(rejects, in promise-speak). We can add error handling by running the .catch()
method of the previous promise.
let errorCase = promise3.catch((error) => {
console.log(`There has been a problem with your fetch operation: ${error.message}`);
});
This allows us to control error handling exactly how we want. In a real app, your catch()
block
could retry fetching the image, or show a default image, or prompt the user to provide another url, or
whatever.
Chaining the blocks together
You can shorten this code and chain together then()
and catch()
blocks. The above
code could be written like this:
fetch('coffee.jpg')
.then(response => response.blob())
.then((myBlob) => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(error => console.log(`Error: ${error.message}`));
The value returned by the executor function passed to a then()
block becomes the parameter
passed to the next then()
block's executor function. Note that .then()
and
.catch()
blocks in promises are the async equivalent of a try...catch block in sync code.
Running code in response to multiple promises fulfilling
Let's look at some more advanced features. What if you want to run some code shortly after a whole bunch of
promises have all fulfilled? You can do this with the Pomise.all()
static method. It takes an
array of promises as parameter and returns a new promise object that will fulfill only if all promises in the
array fulfill:
Promise.all(p1, p2, p3)
.then((values) => {
// do something here
});
If all promises fulfill, the executor function will be passed an array "values as parameter, containing all
the results. If any of the promises passed to Promise.all()
reject, the whole block will reject.
This can be very useful if you're fetching information to dynamically populate a UI. It generally makes sense
to receive all the data rather than displaying partial information.
Here is a real world example:
function fetchAndDecode(url, type) {
return fetch(url).then((response) => {
if (type === 'blob') {
return response.blob();
} else if (type === 'text') {
return response.text();
}
})
.catch(error => console.log(`Fetching error: ${error.message}`));
}
let coffee = fetchAndDecode('coffee.jpg', 'blob');
let tea = fetchAndDecode('tea.jpg', 'blob');
let description = fetchAndDecode('description.txt', 'text');
Promise.all([coffee, tea, description])
.then((values) => {
// store each value returned from the promises in separate variables,
// create object URLs from the blobs
let objectURL1 = URL.createObjectURL(values[0]);
let objectURL2 = URL.createObjectURL(values[1]);
let descText = values[2];
// display the images in <img> elements
let image1 = document.createElement('img');
let image2 = document.createElement('img');
image1.src = objectURL1;
image2.src = objectURL2;
document.appendChild(image1);
document.appendChild(image2);
// display the text in a paragraph
let para = document.createElement('p');
para.textContent = descText;
document.appendChild(para);
});
First, we define a function being passed a url and the type of resource being fetched (blob or text). Inside
the function, we call the fetch()
function to fetch the resource at the specified url. In this
case, the second promise we chain on is different depending on what the type
value is.
Additionally, we don't run the promise chain in isolation, we have added the return
keyword
before the fetch call. The effect is to run the entire chain and then run the final result (the promise
returned by blob()
or text()
) as the return value of the function just defined. In
effect, the return
statements pass the results back up the chain to the top.
At the end of the block, we chain on a .catch()
call, to handle any error that may come up with
any of the promises passed in the array to .all()
. If any of the promises reject, the catch block
will let you know which one had a problem. The .all()
block will still fulfill, but just won't
display the resources that had problems. If you wanted the .all()
to reject, you'd have to chain
the .catch()
block on to the end of it instead.
Next, we call our function three times to begin the process of fetching and decoding the images and text.
Each of the returned promises is stored in a variable. Then we define a Promise.all()
block to
run some code only when all three of the promises stored above have successfully fulfilled. The executor
inside the .then()
will be passed an array containing the results from the individual promises
(decoded response bodies).
Running some final code after a promise fulfills / rejects
There will be cases where you want to run a final block of code after a promise completes, regardless of
whether it fulfilled or rejected. The .finally()
can be chained onto the end of the regular
promise chain, allowing you to cut down on code repetition of adding this final code to the end of both the
.then()
and .catch()
blocks.
myPromise
.then(response => doSomething(response))
.catch(error => handleError(error))
.finally(() => runFinalCode());
In a real example, this would look like this:
function fetchAndDecode(url, type) {
return fetch(url).then((response) => {
if (type === 'blob') {
return response.blob();
} else if (type === 'text') {
return response.text();
}
})
.catch(error => console.log(`Fetch failed: ${error.message}`))
.finally(() => console.log(`Fetch attempt for ${url} finished.`))
}
Building your own custom promises
Combining different promise-based APIs together to create custom functionality is by far the most common way
you'll do custom things with promises. There is another way, however.
Using the Promise() constructor
It is possible to build your own promises using the Promise()
constructor. The main situation in
which you'll want to do this is when you've got code based on an old-school asynchronous API that is not
promise-based, which you want to promis-ify.
Here is a simple example, where we wrap a setTimeout()
with a promise. This runs a function
after two seconds that resolves the promise with a string of "Success!".
let timeoutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Success!");
}, 2000);
});
resolve()
and reject()
are functions that you call to resolve or reject the
newly-created promise. So, when you call this promise, you can chain a .then()
block onto its end
an it will be passed the "Success!" string. In the below code, we simply alert that message:
timeoutPromise()
.then(message => alert(message));
or even just:
timeoutPromise().then(alert);
Rejecting a custom promise
We can create a promise that rejects using the reject()
method, taking a single value: the value
to reject with, i.e. the error passed into the .catch()
block.
function timeoutPromise(message, interval) {
return new Promise((resolve, reject) => {
if (message === '' || typeof message !== 'string') {
reject("Message is empty or not a string.");
} else if (interval < 0 || typeof interval !== 'number') {
reject("Interval is negative or not a number.");
} else {
setTimeout(() => resolve(message), interval);
}
})
}
Here we are passing two values into a custom function, a message to do something with and a time interval
before doing this thing. Inside the function, we return a new Promise
object. Invoking the
function will return the promise we want to use. Inside this Promise constructor, we do a number of checks:
- We check to see if the message is appropriate for being alerted. If it's an empty string or not a string
at all, we reject the promise with a suitable error message
- Next, we check to see if the interval is an appropriate interval value. If it's negative or not a number,
we reject the promise with a suitable error message.
- Finally, if the parameters are ok, we resolve the promise with the specified message after the specified
interval has passed using
setTimeout()
.
Since the timeoutPromise()
function returns a Promise
, we can chain
.then()
, .catch()
blocks onto it to make use of its functionality.
timeoutPromise('Hello there!', 1000)
.then(message => alert(message))
.catch(error => console.log(error));
A more real-world example
In the above example, the async nature is faked using setTimeout()
. One example we'd like to
invite you to study is Jake Archibald's idb library. This takes the IndexedDB API, which is an
old-style callback-based API for storing and retrieving data on the client-side, and allows you to use it with
promises. The following block converts the basice request model used by many IndexedDB methods to use
promises:
function promisifyRequest(request){
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
Conclusion
Promises are a good way to build asynchronous applications when we don't know the return value of a function
or how long it will take to return. They make it easier to express and reason about sequences of asynchronous
operations without deeply nested callbacks. And they support a style of error handling that is similar to the
synchronous try / catch statement. We didn't touch on all promise features in this article, just the most
interesting and useful ones. Most of modern Web APIs are promise-based, so you'll need to understand promises
to get the most out of them. Among those APIs are WebRTC, Web Audio, Media Capture and Streams. Promises will
be more and more important as time goes on, so learning to use and understand them is an important step in
learning modern JavaScript.
Easier async programming with async await
async
functions and the await
keyword are part of the ECMAScript 2017 JavaScript
edition. These features are basically synctatic sugar on top of promises, making async code easier to write
and to read afterward. They make async code look more like old-school sync code.
The basics of async / await
There are two parts to async / await.
The async keyword
First of all, we have the async
keyword, which you put in front of a function declaration to
turn it into an async function. Try typing these lines into the browser's JavaScript console:
function hello() {
return 'Hello!';
}
hello();
The function returns "Hello!", nothing special. But if you turn it into an async function:
async function hello() {
return 'Hello!';
}
hello();
Invoking the function now returns a promise. You can also create an async
function like so:
let hello = async () => 'Hello!';
hello();
To actually consume the value returned when the promise fulfills, since it's returning a promise, we cound
use a .then()
block.
hello()
.then(value => console.log(value));
or even just a shorthand such as:
hello().then(console.log);
The await keyword
The real advantage of the async
functions becomes apparent when you combine it with the
await
keyword. This can be put in front of any async
promise-based function to make
the code execution pause on that line until the promise fulfills, then the fulfillement value is returned.
Here is a trivial example:
async function hello() {
return greeting = await Promise.resolve('Hello!');
}
hello().then(alert);
Rewriting promise code with async await
Let's look back at a simple fetch example.
fetch('coffee.jpg')
.then(response => response.blob())
.then((myBlob) => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.appendChild(image);
})
.catch(error => console.log(`Error while fetching: ${error.message}`));
Let's convert this to use async / await to see how much simpler it makes things.
async function myFetch() {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.appendChild(image);
}
It makes code much simpler and easier to understand. No more .then()
blocks everywhere!
Since the async
keyword turns a function into a promise, you could refactor your code to use a
hybrid approach of promises and await
, bringing the second half of the function out into a new
block, to make it more flexible:
async function myFetch() {
let response = await fetch('coffee.jpg');
return await response.blob();
}
myFetch()
.then((blob) => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
});
But how does it work?
It is necessary to wrap the code inside a function and to include the async
keyword before the
function
keyword. You have to create an async function to define a block of code in which you'll
run your async code; await
only works inside of async functions.
Inside the myFetch()
function, instead of needing to chain a .then()
block on to
the end of each promise-based method, you just need to add an await
keyword before the method
call and then assign the result to a variable. The await
keyword causes the JavaScript parser to
pause on this line until the async call has returned its result, then once it is complete, move on to the next
line. So, for example:
let response = await fetch('coffee.jpg');
The response returned by fetch()
is assigned to a variable when it is available, and the parser
pauses on this line until that occurs. Once the response is available, the parser moves to the next line,
which creates a blob out of it. This line also invokes an async promise-based method, so we use
await
there as well. When the result of the operation returns, we return if out of the
myFetch()
function.
This means that when we call the myFetch()
function, it returns a promise, so we can chain a
.then()
onto the end of it, inside which we handle displaying the blob onscreen.
Adding error handling
If you want to add error handling, you've got a couple of options. You can use the familiar sync try / catch
structure:
async function myFetch() {
try {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
} catch(error) {
console.log(error);
}
}
myFetch();
The catch()
block is passed an error object that can be logged to the console. Using the
refactored hybrid version of the code, you'd be better off chaining a .catch()
block onto the end
of the .then()
block like this:
async function myFetch() {
let response = await fetch('coffee.jpg');
return await response.blob();
}
myFetch()
.then((blob) => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(error => console.log(error));
This is because the .catch()
block will catch errors occurring in both the async function call
and the promise chain. If you used the try / catch block here, you might still get unhandled errors in the
myFetch()
function when called.
Awaiting a promise.all
These keywords, async / await, are compatible with Promise.all()
. So, you can happily await a
Promise.all()
call to get all results returned in a variable in a way that looks like simple sync
code. Let's convert an example we already saw previously:
async function fetchAndDecode(url, type) {
let response = await fetch(url);
let content;
if (type === 'blob') {
content = await response.blob();
} else if (type === 'text') {
content = await response.text();
}
return content;
}
async function displayContent() {
let coffee = fetchAndDecode('coffee.jpg', 'blob');
let tea = fetchAndDecode('tea.jpg', 'blob');
let description = fetchAndDecode('description.txt', 'text');
let values = await Promise.all([coffee, tea, description]);
let objectURL1 = URL.createObjectURL(values[0]);
let objectURL2 = URL.createObjectURL(values[1]);
let descText = values[2];
let image1 = document.createElement('img');
let image2 = document.createElement('img');
image1.src = objectURL1;
image2.src = objectURL2;
document.body.appendChild(image1);
document.body.appendChild(image2);
let para = document.createElement('p');
para.textContent = descText;
document.body.appendChild(para);
}
displayContent()
.catch(error => console.error(error));
Only a few changes were needed. See the Promise.all()
line:
let values = Promise.all([coffee, tea, description]);
By using await
here, we are able to get all the results of the three promises returned into the
values array, when they are all available, in a way that looks very much like sync code.
The downsides of async / await
Async / await is really useful to know about, but there are a couple of downsides to consider.
They make your code look asynchronous, and in a way, it makes it behave more synchronously. The
await
keyword blocks the execution of all the code after it until the promise fulfills, exactly
as it would with a synchronous operation. This means that your code could be slowed down by a significant
number of awaited promises happening straight after another. Each await
will wait after for the
previous one to finish, whereas actually what you want is for the promises to begin processing simultaneously
like they would do if we weren't using async / await.
There is a pattern that can mitigate this problem ‐ setting off all the promise processes by storing the
Promise
object into variables, and then awaiting them afterwards. Let's analyze two examples,
slow and fast async / await, both starting off with a custom function that fakes an async process with a
setTimeout()
call:
function timeoutPromise(interval) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), interval);
});
}
Then each one includes a timeTest()
async function that awaits three
timeoutPromise()
calls:
async function timeTest() {
// function body here
}
Each one ends by recording a start time, seeing how long the timeTest()
promise takes to
fulfill, then recording and end time and reporting how long the operation took in total.
let startTime = Date.now();
timeTest().then(() => {
let finishTime = Date.now();
let timeTaken = finishTime - startTime;
alert(`Time taken in milliseconds: ${timeTaken}`);
})
It is the timeTest()
that differs in each case. In the slow async / await example, it looks like
this:
async function timeTest() {
await timeoutPromise(3000);
await timeoutPromise(3000);
await timeoutPromise(3000);
}
Here, we simply await all three timeoutPromise()
calls directly, making each one alert for 3
seconds. Each subsequent one is forced to wait until the last one finished. If your run the first example,
you'll see the alert box reporting a total run time of about 9 seconds.
In the fast async / await example, timeTest()
looks like this:
const timeoutPromise1 = timeoutPromise(3000);
const timeoutPromise2 = timeoutPromise(3000);
const timeoutPromise3 = timeoutPromise(3000);
await timeoutPromise1;
await timeoutPromise2;
await timeoutPromise3;
Here we store the three Promise
objects in variables, which has the effect of setting off their
associated processes all running simultaneously. Next, we await their results. Because the promises all
started processing at the same time, the promises will all fulfill at the same time.Running the second
example, you'll see the alert box reporting a total run time of just over 3 seconds!
Another minor inconvenience is that you have to wrap your awaited promises inside an async function.
Async / await class methods
You can even add async
in front of class / object methods to make them return promises and
await
promises inside them.
class Person {
constructor(first, last, age, gender, interests) {
this.name = {first, last};
this.age = age;
this.gender = gender;
this.interests = interests;
}
async greeting() {
return await new Promise.resolve(`Hi! I'm ${this.name.first}`);
}
farewell() {
console.log(`${this.name.first} has left the building. Bye for now!`);
}
}
let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
This first class method can be used like this:
han.greeting()
.then(console.log);
Summary
Async / await provide a nice, simplified way to write async code that is simpler to read and maintain. It is
well worth learning and considering for use.