JavaScript Async Await Guide: Complete Tutorial

async and await let you write asynchronous JavaScript that reads like synchronous code, with cleaner error handling and clear execution order.

8 min read

async and await are JavaScript keywords that let you write asynchronous code that looks and reads like synchronous code. Under the hood, they still use promises, but the syntax hides the chaining and makes the flow of values easier to follow.

An async function always returns a promise. Inside it, the await keyword pauses execution until a promise settles, then unwraps the resolved value. You read the code top to bottom, just like synchronous code, even though it is waiting on a network request, a file read, or a timer.

javascriptjavascript
async function fetchUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  const user = await response.json();
  return user;
}
 
fetchUser(1).then((user) => console.log(user.name));

The function pauses at each await, waits for the result, then continues. The caller gets back a promise and uses then, or another await, to get the final value.

async functions always return a promise

Marking a function async does one thing: it makes the function return a promise. Whatever you return from the function becomes the resolved value of that promise.

javascriptjavascript
async function add(a, b) {
  return a + b;
}
 
const result = add(3, 4);
console.log(result); // Promise { 7 }

Even though add returns the plain number 7, the caller receives a promise that resolves to 7, not the number directly. Getting the actual value takes one more step.

javascriptjavascript
add(3, 4).then((sum) => console.log(sum)); // 7
 
// Or inside another async function:
// const sum = await add(3, 4);

This is the single most important rule of async functions. They always wrap the return value in a promise, and there is no way to return a bare value from one.

The await keyword: pause until resolved

The await keyword can only be used inside an async function. It takes a promise, pauses the function, and waits for the promise to settle. A resolved promise hands await its value, and a rejected one makes await throw.

javascriptjavascript
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

This helper wraps setTimeout in a promise so it can be awaited, a pattern covered in more depth in JavaScript setTimeout Behavior: Complete Guide. Using it inside an async function pauses that function without blocking anything else.

javascriptjavascript
async function greet() {
  console.log("Hello");
  await delay(2000);
  console.log("World");
}
 
greet();

Running greet prints the first line immediately, waits two full seconds while the rest of the page stays responsive, then prints the second line.

texttext
Hello
(2 second pause)
World

The await only pauses the greet function itself, not the entire program. Other code on the page keeps running during that two second wait.

How async/await compares to promise chaining

The same logic can be written as a promise chain or as an async function. The table below compares how each style handles structure, naming, and errors before looking at the code itself.

AspectPromise chainingasync/await
StructureCallbacks linked with thenSequential statements with await
NamingValues live only inside callbacksEach step gets its own named variable
ErrorsHandled with a catch at the endHandled with try...catch around the block
Best forShort chains, functional styleLonger flows with branching logic
javascriptjavascript
async function getUserPosts(userId) {
  const userRes = await fetch(`/users/${userId}`);
  const user = await userRes.json();
  const postsRes = await fetch(`/posts?userId=${user.id}`);
  const posts = await postsRes.json();
  return posts.filter((p) => p.published);
}

Each awaited line stores its result in a named variable, so user and posts are readable on their own instead of being buried inside nested callbacks. The equivalent promise chain for this exact function is shown in JavaScript Promise Chaining: A Complete Guide.

Promise chains vs async/await structure

Both chains do the same work in the same order. The diagram lines up each promise-chain step next to its async/await equivalent, showing that the two styles are the same underlying steps written in a different shape.

Error handling with try...catch

The biggest practical benefit of async/await is that you can use a try...catch block for error handling, just like synchronous code. This is often cleaner than chaining multiple catch calls.

javascriptjavascript
async function loadDashboard(userId) {
  try {
    const user = await fetch(`/users/${userId}`).then((r) => r.json());
    const posts = await fetch(`/posts?userId=${userId}`).then((r) => r.json());
    return { user, posts };
  } catch (error) {
    console.error("Dashboard load failed:", error.message);
    return { user: null, posts: [] };
  }
}

Any await that rejects inside the try block jumps straight to catch. One try...catch protects the whole sequence, so you do not need to attach a separate catch to each individual fetch.

You can also handle errors at the call site instead of inside the function itself.

javascriptjavascript
async function main() {
  try {
    const dashboard = await loadDashboard(42);
    render(dashboard);
  } catch (err) {
    showErrorBanner("Could not load dashboard");
  }
}

Since async functions return promises, you can use .catch() on the returned promise instead of try/catch if that reads better for the situation. For a deeper look at structuring async error handling, see Handling Async Errors with Try Catch in JS Guide.

Sequential vs parallel execution

Await serializes promises by default. If you have three independent fetches and await each one in turn, they run one after another instead of at the same time.

javascriptjavascript
// Sequential: runs one at a time (slower)
async function loadSequential() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(1);
  const comments = await fetchComments(1);
  return { user, posts, comments };
}

When the requests do not depend on each other, start them all at once with Promise.all and await the combined result instead.

javascriptjavascript
// Parallel: all three start at once (faster)
async function loadParallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
    fetchComments(1),
  ]);
  return { user, posts, comments };
}

Promise.all starts every promise in the array immediately and resolves once all of them have resolved. If any single one rejects, the whole call rejects right away with that error.

PatternTotal time (3 x 200ms fetches)When to use
Sequential await~600msEach step depends on the previous one
Promise.all~200msAll fetches are independent
Promise.allSettled~200msYou want partial results, not all-or-nothing

Promise.allSettled runs every promise to completion regardless of individual failures, which is useful when a partial result is still valuable.

javascriptjavascript
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(-1), // This will fail
  fetchUser(2),
]);
 
results.forEach((result) => {
  console.log(result.status === "fulfilled" ? result.value.name : result.reason.message);
});

Unlike Promise.all, one rejected fetch here does not stop the others. Each entry in the results array reports its own status, so the loop can print a name or a failure message depending on what happened.

Common async/await mistakes

Forgetting that async functions return promises. Calling an async function and expecting the raw return value is a common beginner error.

javascriptjavascript
async function getName() {
  return "Taylor";
}
 
// Mistake: treating the result as a string
const rawName = getName();
console.log(rawName.toUpperCase()); // Error: rawName.toUpperCase is not a function

Fix it by awaiting the call before using the value, since calling getName on its own hands back a pending promise, not the string itself.

javascriptjavascript
// Correct: await or .then()
const name = await getName();
console.log(name.toUpperCase()); // "TAYLOR"

Using await on non-promises. Await works on any value, not just promises. If the value is not a promise, it is wrapped in a resolved promise and returned right away. This is harmless but unnecessary.

Mixing then and await without reason. Both patterns work, but mixing them in the same function makes the code harder to follow. Picking one style per function keeps the logic easy to scan.

javascriptjavascript
// Confusing mix
async function loadStuff() {
  const user = await fetchUser(1);
  fetchPosts(user.id).then((posts) => console.log(posts));
}
 
// Cleaner: pick one
async function loadStuff() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(user.id);
  console.log(posts);
}

Awaiting inside loops sequentially when parallelism is possible. Looping over an array and awaiting each item one at a time serializes work that could run at the same time instead.

javascriptjavascript
// Slow: one at a time
async function fetchAllUsers(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id));
  }
  return users;
}

The fast version starts every request immediately by mapping the array into a list of promises first, then waits for all of them together instead of waiting on each one before starting the next.

javascriptjavascript
// Fast: all at once
async function fetchAllUsers(ids) {
  const userPromises = ids.map((id) => fetchUser(id));
  return Promise.all(userPromises);
}

Swallowing errors. An empty catch block hides failures silently, which turns a debuggable error into a mystery bug that only shows up much later.

javascriptjavascript
// Mistake: silent failure
try {
  await riskyOperation();
} catch {
  // Nothing. Bug stays hidden.
}

The corrected version keeps the exact same structure but adds one log line inside the catch block, which is often enough to make a hidden failure visible again.

javascriptjavascript
// Better: at least log it
try {
  await riskyOperation();
} catch (error) {
  console.error("riskyOperation failed:", error.message);
}

When to use async/await vs promise chaining

Use async/await whenUse promise chaining when
You need intermediate variable namesThe chain is short and simple
Error handling with try...catch reads betterYou prefer a compact, functional style
You have conditional logic between stepsEach step is a single expression
The flow is complex with branchesYou are mapping over an array of async tasks

Both patterns compile to the same promise machinery underneath. The choice comes down to readability for the task at hand, not performance.

Rune AI

Rune AI

Key Insights

  • async functions always return a promise, even when you return a plain value.
  • await pauses execution inside an async function until the promise settles.
  • Use try...catch to handle errors in async functions the same way as synchronous code.
  • Promises run in parallel when created before awaiting; use Promise.all to coordinate them.
  • Do not mix .then() and await in the same function without a clear reason.
RunePowered by Rune AI

Frequently Asked Questions

Does an async function always return a promise?

Yes. Even if you return a plain value from an async function, JavaScript wraps it in a resolved promise. The caller must use .then() or await to get the value.

Can I use await outside an async function?

In ES2022 and later, you can use top-level await in ES modules (files loaded with type="module"). In CommonJS and older scripts, await is only allowed inside async functions.

Is async/await faster than promise chaining?

No. async/await is syntactic sugar over promises. It compiles to the same promise-based machinery. The benefit is readability, not performance.

Conclusion

async and await do not replace promises. They make promises easier to read and write. An async function always returns a promise, await pauses execution until a promise settles, and try...catch handles errors the same way it handles synchronous ones. Once you are comfortable with both patterns, you can choose the one that reads more naturally for the task.