A less scary explanation of async await
There are a lot of explanations of async
and await
out there, but in my opinion, they're too academic for people who are completely new to the concept. This post is my attempt at a simpler explanation that shows what real world usage looks like. Before we start, here's a quick refresher on the meaning of "async":
Sometimes, we need our code to communicate with another server/the file system/etc. We don't know how long these operations are going to take, so we write the code to be asynchronous – meaning that it's "non-blocking" — that is, other code can continue to run while we wait for the response. Nobody enjoys spelling asynchronous so we abbreviate it as
async
in the code.— from my other post, Acing the JavaScript interview
For the purposes of this tutorial, let's assume that we have an async function called getData
. It uses JavaScript's global fetch
method to make a request to another URL and then log some data to the console. Right now, it looks like this:
function getData() {
const response = fetch("https://jsonplaceholder.typicode.com/posts");
console.log(response);
}
The thing is, if you run that in your DevTools console right now, it'll just log Promise {<pending>}
.
Ugh, what gives?
Well, you know how when you visit a website, it takes a couple seconds to actually load the page? This fetch
request is doing a similar thing — it needs to travel off to some server and retrieve the data because of fetch
. In place of that, it "promises" that it'll come back with your data. The way that function is written, response
is currently just a "promise".
If we want to log the actual response, we need to tell JavaScript to wait for it. To do this, we write the grandiose-sounding word await
before fetch
:
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
// ^^^^^
But we're not done yet. We can only use await
inside of functions marked as "async". In other words, we need to tell JavaScript that getData
is an asynchronous function. To do this, we write the word async
before function
:
async function getData() {
// ^^
Sidenote: if getData
was an arrow function and you wanted to make it asynchronous, you'd write async
before the arguments like const getData = async () => ...
.
All together, your final (non-arrow) function looks like this:
async function getData() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
console.log(response);
}
Now, JavaScript will "smartly" know not to log response
until the posts are retrieved. You'll often hear frontend developers describe this with the phrase, "when the promise resolves".
Those are the basics, but you can stick around if you want to find out more about using the fetch
function. You see, if you run getData
right now, it'll return something like:
Response {type: 'cors', url: 'https://jsonplaceholder.typicode.com/posts', redirected: false, status: 200, ok: true, …}
...which is cool and all, but you'll likely want to transform the posts into JSON format to make them more readable. To do that, you can call the .json()
method on the response, which returns yet another promise, which you can await
:
async function getData() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const json = await response.json();
console.log(json);
}
If you run that function, it'll actually log all of the posts to the console!
That's good enough for many cases, but for the final part of this post, I want to talk about error handling. There are two types of errors that you might want to guard against:
-
Request failures. For instance, this could happen if you lose your internet connection, if there's a CORS error, or if the server doesn't return data in JSON format.
-
Server failures. For example, the request might succeed, but the server could reject it and return an error code like:
{ "error_code": "auth_required", "message": "You must be logged in to do that!" }
To handle #1, you could wrap your code in a try
/catch
block. If you haven't seen those before, they work like this:
try {
// 🍇
} catch (error) {
// if the code in 🍇 throws an error, this will run
}
Adding a try
/catch
block to our getData
function would look like this:
async function getData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const json = await response.json();
console.log(json);
} catch (error) {
console.error(error);
}
}
For #2, If you're expecting an error response via JSON, you could watch the response for the existence of an error_code
and add an early return:
async function getData() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const json = await response.json();
if (json.error_code) {
console.error({ code: json.error_code, text: json.message });
return;
}
console.log(json);
}
Here, we're checking for the existence of an error_code
in the response. If one is there, we'll log the error to the console and stop executing getData
. You're welcome to handle this part however you want — for instance, instead of logging an error to the console, you might want to change your UI to show the error message to the user, or you might want to trigger different behavior depending on which error_code
was returned (like redirecting the user to the login page if they aren't authenticated).
Combining both #1 and #2 would look like this:
async function getData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const json = await response.json();
if (json.error_code) {
console.error({ code: json.error_code, text: json.message });
return;
}
console.log(json);
} catch (error) {
console.error(error);
}
}
Now, you have an asynchronous function that guards against a wide class of errors!
I hope you enjoyed this post. If there's anything that could be explained more clearly, please email me at m@mtm.dev to let me know.