Skip to content
C Codeloom
JavaScript

JavaScript Async/Await Tutorial

Master async and await in JavaScript — write asynchronous code that reads like synchronous code, handle errors with try/catch, and parallelize work with Promise.all.

·4 min read · By Codeloom
Beginner 9 min read

What you'll learn

  • What async functions actually return
  • How await pauses execution without blocking
  • Error handling with try/catch
  • Running tasks in parallel correctly
  • Common async pitfalls to avoid

Prerequisites

  • Familiarity with JavaScript basics

async/await is syntactic sugar over promises, but it transforms how you think about asynchronous code. Instead of chaining then callbacks, you write code that looks linear and synchronous. In this tutorial we will walk through the mechanics and the patterns that matter most.

What async Means

The async keyword on a function does two things. First, it guarantees the function returns a promise. Second, it allows you to use await inside the function body. Even if you return a plain value, it will be wrapped in a resolved promise automatically.

async function getNumber() {
  return 42;
}

getNumber().then(n => console.log(n)); // 42

This means callers always handle the result with then or another await.

How await Works

await pauses execution of the async function until the awaited promise settles. The key insight is that this pause does not block the main thread — the rest of your program keeps running. Under the hood, the function is suspended and resumed when the promise resolves.

async function loadUser(id) {
  const res = await fetch(`/api/users/${id}`);
  const user = await res.json();
  return user;
}

Compare this to the equivalent chained version and you will immediately see why developers prefer it for readable flow control.

Error Handling with try/catch

One of the biggest wins of async/await is unified error handling with normal try/catch blocks. Any rejection becomes a thrown error, so you can use familiar synchronous patterns.

async function safeLoad(id) {
  try {
    const user = await loadUser(id);
    return user;
  } catch (err) {
    console.error('Failed to load user', err);
    return null;
  }
}

You can also use finally for cleanup, just like with synchronous code.

Sequential vs Parallel

A common mistake is awaiting promises sequentially when they could run in parallel. Each await waits for the previous one, so independent calls become slow.

// Slow: 2 seconds total if each takes 1
const a = await fetchA();
const b = await fetchB();

// Fast: 1 second total
const [a2, b2] = await Promise.all([fetchA(), fetchB()]);

Always start independent promises first, then await them together. This single habit can speed up an app dramatically.

Top-Level Await

Modern JavaScript modules support top-level await. You can use it directly at the top of an ES module without wrapping the code in an async function.

// In an ES module
const config = await fetch('/config.json').then(r => r.json());
export default config;

This is great for module initialization, but be aware that it delays the module’s loading, which cascades to dependents.

Loops and async

Using await inside a for loop processes items one at a time. Using Array.prototype.forEach with an async callback does not wait at all — forEach ignores the returned promises. Choose carefully.

// One at a time
for (const id of ids) {
  await processOne(id);
}

// All at once
await Promise.all(ids.map(id => processOne(id)));

When you need controlled concurrency (say, ten at a time), reach for a small helper like p-limit or write a queue.

Pitfalls to Avoid

Three traps catch newcomers. First, returning a promise from inside a try without awaiting it means the catch will never see its rejection. Always return await if you need the catch to fire.

Second, async functions in array methods like filter or map return promises, not booleans or values. You usually need Promise.all afterwards.

Third, do not forget that async always returns a promise. If a caller forgets to await, errors become unhandled rejections that may crash your Node process.

Wrapping Up

Async/await turns asynchronous control flow into ordinary-looking code while preserving all the power of promises. Combine it with Promise.all for parallelism and try/catch for clean error handling, and your async code will be both fast and maintainable.