JavaScript’s async and await functions make for readable and maintainable asynchronous code. Just watch out for their downsides. Credit: Monkey Business Images/Shutterstock One of the nicest improvements to developer experience in recent JavaScript history is the introduction of the async and await keywords, introduced in ES2017. Together, these elements make it easy to write asynchronous logic with a simple syntax. Under the hood, they use promises, but for many cases, async and await will help you write more readable and maintainable asynchronous code without thinking about implementation. In this article, we’ll first look at how to use async await in your JavaScript programs. Then, we’ll talk about some of the downstream implications of using the syntax. JavaScript async and await defined To start, let’s get a working definition of the async and await JavaScript keywords: async: Declares a function as one that will contain the await keyword within it. await: Consumes an asynchronous function that returns a promise with synchronous syntax. Consuming a JavaScript promise Possibly the most common use case in all of JavaScript for async and await is using the browser’s built-in fetch API. Listing 1 declares an async function and uses await within it. The purpose is to pull some JSON data from the public Star Wars API. Listing 1. JavaScript async/await and the fetch API async function getStarWarsData() { try { const response = await fetch('https://swapi.dev/api/people/1/'); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } } getStarWarsData(); // Returns: {name: 'Luke Skywalker', height: '172', mass: '77', hair_color: 'blond', skin_color: 'fair', …} Notice that you can get hold of the promise returned by a method like fetch(); in this case, the response object. response.json() is also an asynchronous method returning a promise, so we again use await. Our getStarWarsData() function must be prefixed with the async keyword— otherwise, the browser will not allow await in its body. Refactoring with async and await Now, let’s look at a more complex example. To start, we’ll lay out a simple JavaScript program using promises, then we’ll refactor it to use async and await. In our example, we’ll use axios, a promise-based HTTP library. Here’s the relevant snippet: Listing 2. The promise-based example using axios let getUserProfile = (userId) => { return axios.get(`/user/${userId}`) .then((response) => { return response.data; }) .catch((error) => { // Do something smarter with `error` here throw new Error(`Could not fetch user profile.`); }); }; getUserProfile(1) .then((profile) => { console.log('We got a profile', profile); }) .catch((error) => { console.error('We got an error', error) }); Listing 2 defines a function that takes a user ID and returns a promise (the return type of axios.get().then().catch()). It calls an HTTP endpoint to get profile information and will resolve with a profile or reject with an error. This works fine, but the abundance of syntax inside getUserProfile clutters our business logic, as shown in Listing 3. Listing 3. Clutter in the getUserProfile method .then((response) => { /* important stuff */ }) .catch((error) => { /* more important stuff */ }); Now, let’s see what we can do to simplify this program. Step by step with async/await The async and await functions let you work with promises without all of the then and catch syntax promises usually require. In essence, they make your asynchronous code readable. The async and await keywords let you write code that behaves asynchronously but reads synchronously. Whenever the JavaScript interpreter sees an await, it’ll stop execution, go perform the task, and then return as though it were a normal call. This makes for code that is readable and clean, but it does limit the ability to orchestrate complex concurrency. In those cases, you need access more directly to the promises that underlie async/await and the initiation and resolution of the tasks. When added before an expression that evaluates to a promise, await waits for the promise to resolve, after which the rest of the function continues executing. The await function can only be used inside async functions, which are the functions preceded by the async operators. If a promise is fulfilled with a value, you can assign that value like so: Listing 4. Assigning a value to a promise let someFunction = async () => { let fulfilledValue = await myPromise(); }; If the promise is rejected with an error, the await operator will throw the error. Let’s rewrite the getUserProfile function step-by-step using async and await. Step 1. Add the async keyword to our function Adding the async keyword makes a function asynchronous. This allows us to use the await operator in the function body: let getUserProfile = async (userId) => { /* business logic will go here */ }; Step 2. Add the await keyword to the promise Next, we use the await keyword on our promise and assign the resolved value: let getUserProfile = async (userId) => { let response = await axios.get(`/user/${userId}`); /* more to do down here */ } Step 3. Return the value We want to return the data property of our resolved response value. Instead of having to nest it within a then block, we can simply return the value: let getUserProfile = async (userId) => { let response = await axios.get(`/user/${userId}`); return response.data; /* more to do down here */ } Step 4. Add error handling If our HTTP fails and the promise gets rejected, the await operator will throw the rejected message as an error. We need to catch it and re-throw our own error: let getUserProfile = async (userId) => { try { let response = await axios.get(`/user/${userId}`); return response.data; } catch (error) { // Do something smarter with `error` here throw new Error(`Could not fetch user profile.`); } }; JavaScript async/await gotchas We’ve cut down on the amount of syntax we use by a few characters, but more importantly we can read through our code line-by-line as if it were synchronous code. But we do still need to watch out for the following sticky spots. Adding async quietly changes your function’s return value I’ve seen a lot of confusion resulting from turning to async/await in order to add asynchronous calls to a previously synchronous function. You might think you can add async and await and everything else will continue working as expected. The issue is that async functions return promises. So, if you change a function to be asynchronous, you need to make sure that any existing function calls are adjusted to appropriately handle a promise. To be fair, checking pre-existing function calls would also be a thing you need to do if you use traditional promise syntax, but asynchronous functions aren’t as obvious about it. Using async/await means having to deal with a promise Once you’ve added an asynchronous function somewhere, you’re in promise land. You will need to make sure any functions that call the async function either handle the result as a promise or use async/await themselves. If the async function is deeply nested, then the stack of functions that lead to that function call might also be async functions. Again, this issue isn’t specific to async/await and would be a problem with promises, as well. However, at some point, you need to have a promise. If you keep bubbling async/await up through the call stack, eventually you get to the global context where you can’t use await. Somewhere, you’ll have to deal with an async function in the traditional, promise way. Using async/await requires also using try/catch If you’re using async to handle any promise that could possibly be rejected, you’re going to have to also use try/catch, which is not a frequently used language feature. I’ve seen it used almost exclusively within libraries where the only way to detect certain features is by trying them out and catching errors accordingly. This is, of course, a generalization, but my point is that it’s possible to write solid, idiomatic JavaScript for years without having to use try/catch. Adopting async/await requires you to also adopt try/catch. Conclusion This article introduced the async and await keywords in JavaScript. I’ve shown you how this syntax is used in both a simple and a more complex example involving promises. I’ve also discussed some of the gotchas to look out for when using async/await in JavaScript programs. While there are a few drawbacks to using async/await, it can greatly improve the readability of code that relies on promises. Related content feature 14 great preprocessors for developers who love to code Sometimes it seems like the rules of programming are designed to make coding a chore. Here are 14 ways preprocessors can help make software development fun again. By Peter Wayner Nov 18, 2024 10 mins Development Tools Software Development feature Designing the APIs that accidentally power businesses Well-designed APIs, even those often-neglected internal APIs, make developers more productive and businesses more agile. By Jean Yang Nov 18, 2024 6 mins APIs Software Development news Spin 3.0 supports polyglot development using Wasm components Fermyon’s open source framework for building server-side WebAssembly apps allows developers to compose apps from components created with different languages. By Paul Krill Nov 18, 2024 2 mins Microservices Serverless Computing Development Libraries and Frameworks news Go language evolving for future hardware, AI workloads The Go team is working to adapt Go to large multicore systems, the latest hardware instructions, and the needs of developers of large-scale AI systems. By Paul Krill Nov 15, 2024 3 mins Google Go Generative AI Programming Languages Resources Videos