JavaScript • Asynchronous JavaScript

Async/Await — Use async/await for asynchronous code

Write asynchronous JavaScript that reads like simple, step-by-step code using async and await.

JavaScript emblem with code brackets, braces, and a code file.
Inside this lesson 10 sections 5 questions 8 figures

SECTION 1

Historical Context & Motivation

JavaScript was originally designed to run small scripts inside web browsers, but as websites became full-blown applications, developers needed a way to perform tasks that take time — like fetching data from a server or reading a file — without freezing the entire page. This challenge is called asynchronous programming, and the language's approach to it has evolved dramatically over the past decade.

In the early days, developers relied on callbacks — functions passed into other functions to be called later. While callbacks worked, nesting them deeply led to messy, hard-to-read code often called callback hell. The JavaScript community recognized this pain point and sought cleaner patterns.

2009 • Node.js & Callback-Heavy Code

Node.js brought JavaScript to the server, making asynchronous I/O essential. Callbacks became the standard pattern, but deeply nested callbacks made code difficult to maintain.

2012 • Promise Libraries Emerge

Libraries like Bluebird and Q introduced Promises to the JavaScript ecosystem, offering a chainable alternative to raw callbacks and enabling cleaner error handling.

2015 • ES6 Standardizes Promises

ECMAScript 2015 (ES6) added native Promise objects to the language, giving every JavaScript engine a built-in way to represent a future value.

2017 • ES2017 Introduces Async/Await

The async and await keywords landed in ES2017, allowing developers to write asynchronous code that looks and behaves like synchronous, top-to-bottom code.

2022 • Top-Level Await

ES2022 enabled using await at the top level of modules, removing the need to wrap everything inside an async function in modern JavaScript environments.

The key question that async/await answers is this: How can we write code that waits for slow operations without blocking everything else — and still keep the code easy to read? Understanding this journey from callbacks to Promises to async/await will help you appreciate why modern JavaScript looks the way it does.

SECTION 2

Core Principles & Definitions

Before diving into syntax, you need to understand a few foundational ideas. Async/await is built on top of Promises, so these concepts work together like layers in a stack. Think of Promises as the engine under the hood, and async/await as the steering wheel and pedals you actually interact with.

1

Asynchronous Operation

Any task that takes an unpredictable amount of time to complete — such as fetching data from an API, reading a file, or waiting for a timer. JavaScript doesn't pause; it moves on and comes back when the task finishes.
2

Promise

An object representing a value that may not exist yet. A Promise is in one of three states: pending (still working), fulfilled (success), or rejected (error).
3

The async Keyword

Placed before a function declaration, async tells JavaScript that this function will always return a Promise. Even if you return a plain value, it gets wrapped in a Promise automatically.
4

The await Keyword

Used inside an async function, await pauses the function's execution until the Promise it's waiting on settles. The rest of the program keeps running — only this function pauses.
5

Try/Catch for Error Handling

When an awaited Promise rejects, the error is thrown like a regular exception. You wrap your await calls in a try/catch block to handle failures gracefully, just like you would with synchronous errors.
✦

KEY TAKEAWAY

Think of an async function like ordering food at a restaurant. You place your order (start the async operation), and then you're free to chat with friends or check your phone (the program keeps running). When the waiter says "your food is ready" (the Promise resolves), you await that moment and start eating. You didn't have to stand in the kitchen and watch the chef — you just waited for the result.

SECTION 3

Visual Explanation — How Async/Await Flows

The diagram below shows how JavaScript's event loop handles an async function. When the engine hits an await keyword, it suspends the async function and returns control to the main thread. Once the awaited Promise settles, the function resumes right where it left off.

Async/Await Execution Flow1. Call async fn2. Execute until await3. Pause functionMain Thread (free!)Other code keeps runningBackground Taskfetch(), setTimeout(), etc.4. Promise settles5. Resume async fn6. Use resolved valueThe main thread is never blocked — only the async function pauses.
Steps 1–2 show the function executing normally until it hits await. At step 3, the function pauses while the main thread stays free (left box). The background task completes at step 4, and the function picks up right where it left off at steps 5–6.

The critical insight here is that await does not freeze the entire program. It only suspends the specific async function it's inside. The browser can still respond to clicks, animations keep running, and other code executes without delay. When the awaited Promise resolves, JavaScript's event loop schedules the suspended function to resume on the next available tick.

SECTION 4

How Async/Await Works Under the Hood

Async/await is syntactic sugar over Promises — it doesn't add new capabilities, but it makes existing capabilities much easier to use. Understanding what happens behind the scenes will help you debug issues and write better code.

The async Keyword — What It Actually Does

When you mark a function with async, JavaScript guarantees two things. First, the function will always return a Promise, even if you return a simple number or string. Second, you gain the ability to use await inside that function. Here's the equivalence:

ASYNC RETURN EQUIVALENCE

asyncfunctiongreet()return"hello";//isequivalentto:functiongreet()returnPromise.resolve("hello");async function greet() { return "hello"; } // is equivalent to: function greet() { return Promise.resolve("hello"); }

Both versions return a Promise that resolves to the string "hello". The async keyword automates the wrapping for you.

The await Keyword — Pausing and Resuming

The await keyword takes a Promise and unwraps it. If the Promise is already resolved, await returns the value immediately. If it's still pending, the async function suspends and yields control back to the event loop. Once the Promise settles, the function resumes and the resolved value is returned by the await expression.

AWAIT TRANSLATION

constdata=awaitfetch("/api/users");//issimilarto:fetch("/api/users").then(result=>data=result;);const data = await fetch("/api/users"); // is similar to: fetch("/api/users").then(result => { data = result; });

The await version reads top-to-bottom, while the .then() version uses a callback. Both achieve the same result, but await is far easier to follow.

Error Handling with try/catch

When an awaited Promise rejects, the rejection is thrown as an exception inside the async function. This means you can use the same try/catch blocks you already know from synchronous JavaScript. Without a try/catch, an unhandled rejection will bubble up as an unhandled Promise rejection, which is like an uncaught error — the browser or Node.js will warn you.

ERROR HANDLING PATTERN

asyncfunctionloadUser()tryconstresponse=awaitfetch("/api/user");constdata=awaitresponse.json();returndata;catch(error)console.error("Failedtoloaduser:",error);async function loadUser() { try { const response = await fetch("/api/user"); const data = await response.json(); return data; } catch (error) { console.error("Failed to load user:", error); } }

If either fetch() or response.json() rejects, execution jumps straight to the catch block — just like a thrown error in synchronous code.

SECTION 5

Common Async/Await Patterns

Once you've mastered the basics, you'll encounter situations where you need to run multiple asynchronous tasks together, or handle them in a specific order. The diagram below illustrates the two most important patterns: sequential execution (one after another) and parallel execution (all at once).

Sequential (Slower)Parallel (Faster)const a = await fetchA();⏳ wait ~2 secondsconst b = await fetchB();⏳ wait ~2 secondsconst c = await fetchC();⏳ wait ~2 secondsTotal: ~6 secondsconst [a,b,c] = await Promise.all([fetchA()⏳ ~2sfetchB()⏳ ~2sfetchC()⏳ ~2sAll three run simultaneously!Total: ~2 secondsUse sequential when tasks depend on each other; use parallel when they're independent.
On the left, three sequential await calls take ~6 seconds total because each waits for the previous one. On the right, Promise.all() fires all three at once, finishing in ~2 seconds — the time of the slowest single task.

When to Use Each Pattern

PatternSyntaxBest When
Sequentialawait a; await b;Each task depends on the result of the previous one (e.g., fetch a user, then fetch their posts).
Parallelawait Promise.all([a, b])Tasks are independent of each other and can safely run at the same time.
Raceawait Promise.race([a, b])You only need the first result — useful for timeouts or choosing the fastest data source.

Common async/await execution patterns

SECTION 6

Worked Example — Fetching Weather Data

Let's build a real-world example step by step. Imagine you're creating a weather dashboard that fetches a city's current temperature from an API and displays it to the user. We'll use async/await with proper error handling.

Building an Async Weather Fetcher

  1. 1

    Step 1 — Define the Async Function

    Start by creating a function marked with async. This tells JavaScript that the function will contain await expressions and will return a Promise.
    async function getWeather(city) { }
  2. 2

    Step 2 — Fetch Data with Await

    Inside the function, use await fetch() to send a network request. The fetch function returns a Promise that resolves to a Response object. We await it so we can work with the response directly instead of chaining .then() calls.
    const response = await fetch(`https://api.weather.com/v1/${city}`);
  3. 3

    Step 3 — Check for Errors

    The fetch function doesn't reject on HTTP errors like 404 or 500. We need to manually check response.ok and throw an error if the request failed. This way, our catch block will handle both network errors and bad HTTP responses.
    if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); }
  4. 4

    Step 4 — Parse the JSON Response

    Calling response.json() returns another Promise that resolves to the parsed JSON data. We await this too, so data holds a plain JavaScript object we can use immediately.
    const data = await response.json();
  5. 5

    Step 5 — Wrap Everything in try/catch

    Here's the complete function. The try block contains our happy path, and the catch block handles any error — whether it's a network failure, a bad status code, or a JSON parsing issue.
    async function getWeather(city) { try { const response = await fetch(`https://api.weather.com/v1/${city}`); if (!response.ok) throw new Error(`HTTP error: ${response.status}`); const data = await response.json(); return `${data.city}: ${data.temperature}°F`; } catch (error) { console.error("Weather fetch failed:", error.message); return "Unable to load weather data."; } }
  6. 6

    Step 6 — Call the Function

    Since getWeather is async, calling it returns a Promise. We can await it from another async function, or use .then() if we're outside an async context. Notice how the entire flow reads like synchronous code — top to bottom, no indentation pyramids.
    const result = await getWeather("NewYork"); console.log(result); // "NewYork: 72°F"

SECTION 7

Callbacks vs. Promises vs. Async/Await

You can write asynchronous JavaScript using any of three approaches: callbacks, Promises with .then(), or async/await. Each has trade-offs. The table below compares them across the factors that matter most when you're writing and maintaining code.

FeatureCallbacksPromises (.then)Async/Await
ReadabilityPoor — deeply nested "pyramid of doom"Better — flat chains, but still uses callbacks in .then()Best — reads like synchronous, top-to-bottom code
Error HandlingManual — pass error as first argument to callbackUse .catch() at the end of the chainUse familiar try/catch blocks
DebuggingDifficult — stack traces are confusingModerate — improved stack tracesEasy — breakpoints work naturally, clear stack traces
Chaining Multiple StepsRequires nesting within nestingChain with .then().then().then()Sequential await statements on separate lines
Browser SupportUniversal — works everywhereES6+ (all modern browsers)ES2017+ (all modern browsers)

Comparison of asynchronous patterns in JavaScript

✦

KEY TAKEAWAY

Async/await doesn't replace Promises — it's built on top of them. Think of it this way: Promises are like the engine in a car, and async/await is the automatic transmission. You can still shift manually (use .then()), but most of the time, the automatic version is easier and less error-prone. Knowing both lets you pick the right tool for each situation.

SECTION 8

Connection to Advanced Async Patterns

Async/await is your foundation, but as your projects grow more complex, you'll encounter advanced patterns that build on these ideas. Understanding where async/await fits in the bigger picture will prepare you for more sophisticated JavaScript development.

What You Know NowWhat Comes NextWhy It Matters
await a single Promisefor await...of loops (async iterators)Process streams of data one piece at a time — like reading a large file line by line without loading it all into memory.
Promise.all() for parallel tasksPromise.allSettled() for resilient parallel tasksUnlike Promise.all, allSettled waits for every Promise to finish, even if some reject — useful when you want partial results.
try/catch inside one functionGlobal error boundaries and middlewareIn frameworks like Express.js or React, centralized error handling catches async failures across your entire application.
Simple async functionsGenerators and coroutinesAsync/await was actually inspired by generator functions (function*). Understanding generators reveals how the language pauses and resumes execution.

From async/await basics to advanced patterns

Don't feel pressured to learn all of these immediately. The core async/await pattern you've learned in this lesson covers the vast majority of real-world use cases. As you build more complex applications — perhaps a full-stack project with Node.js or a dynamic front-end with React — you'll naturally encounter situations that call for these more advanced tools.

💡

Pro Tip

A common mistake is to make everything sequential with await when tasks could run in parallel. Before writing await, ask yourself: "Does this task need the result of the previous one?" If not, consider Promise.all() to speed things up.

SECTION 9

Practice Problems

Test your understanding of async/await with these five problems, starting with conceptual questions and building up to more challenging scenarios. Try to answer each one before reading the solution.

Question 1: CONCEPTUAL

What does the async keyword do when placed before a function declaration? What does the function return even if you write return 42 inside it?
Show answer
The async keyword transforms a function so that it always returns a Promise. Even if you write return 42, the function actually returns Promise.resolve(42). This automatic wrapping is what allows the await keyword to be used inside it, and it ensures callers can always use .then() or await on the function's return value.

Question 2: BASIC CALCULATION

Consider this code: async function example() { console.log("A"); const result = await Promise.resolve("B"); console.log(result); console.log("C"); } example(); console.log("D"); What is the exact order of the console output (A, B, C, D)?
Show answer
The output order is: A, D, B, C. First, "A" prints because it appears before the await. Then the await pauses the async function and returns control to the caller, so "D" prints next. Once the event loop processes the resolved Promise, the function resumes and prints "B" (the resolved value), followed by "C". This demonstrates how await yields control to the rest of the program.

Question 3: INTERMEDIATE

Rewrite the following Promise chain using async/await with try/catch error handling: function getUserPosts(userId) { return fetch(`/api/users/${userId}`) .then(res => res.json()) .then(user => fetch(`/api/posts?author=${user.name}`)) .then(res => res.json()) .catch(err => console.error(err)); }
Show answer
Here's the equivalent async/await version: async function getUserPosts(userId) { try { const userRes = await fetch(`/api/users/${userId}`); const user = await userRes.json(); const postsRes = await fetch(`/api/posts?author=${user.name}`); const posts = await postsRes.json(); return posts; } catch (err) { console.error(err); } } Notice how each step is on its own line, variables are clearly named, and the error handling uses a familiar try/catch block. The sequential await calls are appropriate here because each request depends on data from the previous one.

Question 4: APPLIED

You're building a dashboard that loads data from three independent API endpoints: /api/sales, /api/customers, and /api/inventory. Each request takes about 1 second. Write an async function that fetches all three as fast as possible and returns an object with the combined data. How long will it take?
Show answer
Since the three requests are independent, use Promise.all() for parallel execution: async function loadDashboard() { try { const [salesRes, custRes, invRes] = await Promise.all([ fetch('/api/sales'), fetch('/api/customers'), fetch('/api/inventory') ]); const [sales, customers, inventory] = await Promise.all([ salesRes.json(), custRes.json(), invRes.json() ]); return { sales, customers, inventory }; } catch (error) { console.error('Dashboard load failed:', error); } } This takes approximately 1 second total (the longest single request), not 3 seconds. We use Promise.all twice: once for the fetch calls and once for the JSON parsing, since both sets of operations are independent.

Question 5: CRITICAL THINKING

A student writes this code to fetch five URLs and collect results, but it's running much slower than expected. Identify the performance problem and suggest a fix: async function fetchAll(urls) { const results = []; for (const url of urls) { const response = await fetch(url); const data = await response.json(); results.push(data); } return results; }
Show answer
The problem is that each await inside the for loop forces the next iteration to wait until the current one completes. If each fetch takes 1 second, five URLs will take 5 seconds sequentially. Since the fetches don't depend on each other, the fix is to start all requests simultaneously using Promise.all() with map(): async function fetchAll(urls) { const results = await Promise.all( urls.map(async (url) => { const response = await fetch(url); return response.json(); }) ); return results; } Now all five fetches run in parallel, completing in about 1 second. The key insight is: using await inside a loop creates sequential behavior. When tasks are independent, use Promise.all with map to parallelize them.

SUMMARY

Lesson Summary

Summary

JavaScript's async/await syntax lets you write asynchronous code that reads from top to bottom, just like synchronous code. The async keyword marks a function as asynchronous and guarantees it returns a Promise. The await keyword pauses the function until a Promise settles, then returns the resolved value — all without blocking the rest of your program. Errors are handled using familiar try/catch blocks, making async error handling feel natural.

For tasks that depend on each other, use sequential awaits — one after another. For independent tasks, combine async/await with Promise.all() to run them in parallel and significantly reduce total wait time. Async/await evolved from callbacks and Promise chains — it doesn't replace them but provides a cleaner interface on top. Mastering this syntax is essential for modern JavaScript development, from front-end frameworks to server-side Node.js applications.

Varsity Tutors • JavaScript • Async/Await — Use async/await for asynchronous code