6. Async/Await
Follow along with code examples here!
Table of Contents:
Key Concepts
asyncfunction — a function that always returns a Promise. Lets you useawaitinside it.await— used inside anasyncfunction; pauses until the Promise resolves, then gives you the resolved value. Makes async code look like synchronous code.try/catch— run code intry; if any of it throws (including fromawait), execution jumps tocatchso you can handle the error instead of crashing.
Intro: Promises Are Tricky
Take a look at this code in src/fetch-helpers.js. What's missing from with this code? Why does it produce the error "Cannot read properties of undefined (reading 'map')"?
Answer
We forgot to return promiseToExtractData from the first .then callback. This means that the first .then() returns a promise that resolves to undefined in the second .then() callback.
So many .then() calls
.then() callsWhen fetching, it is important to observe that we have two sequential asynchronous processes involved, each with their own promise and each with their own .then():
fetch()returns a promise handled by the first.then()When
fetch()resolves, we start extracting data withresponse.json()which returns another promise handled by the second.then().When
response.json()resolves, we do some data manipulation and return the formatted data. This is the final return statement.
We have to also remember that the entire getUsers() function is performing these asynchronous operations so it is also inherently asynchronous. When we return from getUsers(), we return a promise that the main code must resolve. So many promises to resolve! So many .then() calls!
There must be an easier way!
Async / Await
Working with callbacks can be conceptually difficult to follow at times. So, an alternate syntax was created to achieve the same effect but without the need for callbacks.
This approach utilizes the async and await keywords:
The
awaitkeyword is placed before any expression that returns a PromiseIt causes our code to pause and wait for the Promise to resolve.
It then "unpacks" the Promise and returns the resolved value.
The
asynckeyword is placed before any function that usesawaitinside:Any value returned by this function now will be wrapped in a Promise
What about Errors? Try and Catch
This entire process is very error prone in ways that we can't avoid:
fetch()throws an error if the given URL is invalid or your network is down.response.json()throws an error if theresponsedata is not in JSON formatWe manually throw our own error using
throw new Error()when the fetch fails due to a 4xx or 5xx response:
All of these errors crash the program, which we want to avoid.
try/catch is the standard way to handle those errors and it will prevent the application from crashing:
Note with either syntax, our main code doesn't need to catch anything. Our main code simply checks if the data is present or if it was null in which case we can render an error message to the screen.
The benefits of async/await
async/awaitUsing the async/await syntax with try and catch has a number of benefits. The main ones are readability and debuggability.
We can write async code in a synchronous-like manner
We avoid having to write a bunch of callbacks
We can avoid common mistakes made when using callbacks
try/catchis a more general-purpose way of handling errors that can be used for more than just fetching.
Best Practice: Standardize the Return Structure
There is still one way to improve getUsers().
We are currently telling the caller of the function (the main code) that an error occurred by returning null:
What if we wanted to return the error itself? This could be useful if we want our main code to display the exact error message to the user.
One common solution to this is to always return an object with data and error properties:
If the fetch succeeds,
errorwill benull:{ data: users, error: null }If the fetch fails,
datawill benull:{ data: null, error: error }
This allows the main code to check the .data and .error properties to determine what to do next:
Q: Why can't we simply return the users if the fetch succeeds and the error if it fails?
There won't be an easy way to differentiate the two if result can be either a users array or an error object.
In both cases, we have a non-null value so if (result === null) checks won't work.
We could do something like if (result instanceof Error) to determine if the value is an Error object but it is entirely possible that the API we are fetching from has data in an error-like object.
This strategy avoids any ambiguity. It ensures that the data property contains the data we're fetching and the error property contains an error if one occurred.
Best Practice: Fetch Helper Functions
Since most of this code is "boilerplate", it is a good practice to create a helper function that handles most of this fetching logic for us:
We can also make helpers to create config objects for us:
We can use these functions like this to really simplify the process of writing functions for specific endpoints / HTTP methods:
Last updated