DEV Community

楊東霖
楊東霖

Posted on • Originally published at devtoolkit.cc

JavaScript Promises and Async/Await: Complete Guide

Asynchronous programming is at the heart of JavaScript. Whether you're fetching data from an API, reading files, or querying a database, your code needs to handle operations that take time without blocking the rest of the program. Promises and async/await are the modern, clean way to do this — and understanding them deeply will make you a significantly better JavaScript developer.

The Problem: Callback Hell

Before Promises, JavaScript used callbacks. They work fine for simple cases, but nesting them creates deeply indented, hard-to-read code:

// Callback hell — don't do this
getData(function(err, data) {
  if (err) return handleError(err);
  processData(data, function(err, result) {
    if (err) return handleError(err);
    saveResult(result, function(err, saved) {
      if (err) return handleError(err);
      notifyUser(saved, function(err) {
        if (err) return handleError(err);
        console.log('Done!');
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Promises were introduced in ES2015 to solve this. Async/await, added in ES2017, is syntactic sugar on top of Promises that makes asynchronous code look synchronous.

Understanding Promises

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It has three states:

  • Pending: initial state, neither fulfilled nor rejected
  • Fulfilled: the operation completed successfully
  • Rejected: the operation failed

Once a promise settles (fulfills or rejects), it cannot change state.

Creating a Promise

const myPromise = new Promise((resolve, reject) => {
  // Simulate async work
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve('Operation succeeded!');
    } else {
      reject(new Error('Operation failed'));
    }
  }, 1000);
});
Enter fullscreen mode Exit fullscreen mode

The function passed to new Promise() is called the executor. It runs immediately and receives two functions: resolve (call when done) and reject (call on failure).

Consuming Promises with .then() and .catch()

myPromise
  .then(result => {
    console.log('Success:', result);
    return result.toUpperCase(); // return value is passed to next .then()
  })
  .then(upperResult => {
    console.log('Transformed:', upperResult);
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('This runs regardless of success or failure');
  });
Enter fullscreen mode Exit fullscreen mode

Key rules for promise chaining:

  • Each .then() returns a new promise
  • Whatever you return from a .then() callback becomes the resolved value of the next promise
  • If you return a promise from .then(), the chain waits for it
  • A single .catch() at the end handles errors from any step in the chain

Promise Chaining in Practice

// Fetching user data then their posts
fetch('/api/users/1')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then(user => {
    console.log('User:', user.name);
    return fetch(`/api/users/${user.id}/posts`);
  })
  .then(response => response.json())
  .then(posts => {
    console.log(`Found ${posts.length} posts`);
  })
  .catch(error => {
    console.error('Failed:', error.message);
  });
Enter fullscreen mode Exit fullscreen mode

Async/Await

Async/await lets you write promise-based code that looks and reads like synchronous code. Under the hood, it's still promises — just with cleaner syntax.

The async keyword

Adding async before a function makes it return a Promise automatically. Even if you return a plain value, it gets wrapped in Promise.resolve().

async function greet() {
  return 'Hello!';
}

// These are equivalent:
greet().then(msg => console.log(msg)); // 'Hello!'
Promise.resolve('Hello!').then(msg => console.log(msg));
Enter fullscreen mode Exit fullscreen mode

The await keyword

await pauses execution of the async function until the promise settles. It can only be used inside an async function (or at the top level in ES modules).

async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  const user = await response.json();
  return user;
}

// Call it
fetchUser(1)
  .then(user => console.log(user.name))
  .catch(err => console.error(err.message));

// Or use await at the top level (ES modules / Node.js 14.8+)
const user = await fetchUser(1);
console.log(user.name);
Enter fullscreen mode Exit fullscreen mode

The same chain from earlier, rewritten with async/await:

async function loadUserPosts(userId) {
  const userResponse = await fetch(`/api/users/${userId}`);
  if (!userResponse.ok) throw new Error(`HTTP error: ${userResponse.status}`);

  const user = await userResponse.json();
  console.log('User:', user.name);

  const postsResponse = await fetch(`/api/users/${user.id}/posts`);
  const posts = await postsResponse.json();

  console.log(`Found ${posts.length} posts`);
  return posts;
}
Enter fullscreen mode Exit fullscreen mode

Error Handling with try/catch

async function safeFetch(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return await response.json();
  } catch (error) {
    if (error.name === 'TypeError') {
      // Network error or invalid URL
      console.error('Network error:', error.message);
    } else {
      // HTTP error
      console.error('Request failed:', error.message);
    }
    throw error; // Re-throw so callers can handle it
  }
}

// Using the function
async function main() {
  try {
    const data = await safeFetch('/api/data');
    console.log(data);
  } catch (error) {
    // Handle at the call site
    showErrorToUser(error.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Promise Combinators

When you need to run multiple async operations, JavaScript provides several static methods on the Promise class.

Promise.all()

Runs all promises in parallel. Resolves when all of them succeed, or rejects immediately if any of them fail.

async function loadDashboard(userId) {
  // All three requests run in parallel — much faster than sequential
  const [user, posts, notifications] = await Promise.all([
    fetch(`/api/users/${userId}`).then(r => r.json()),
    fetch(`/api/users/${userId}/posts`).then(r => r.json()),
    fetch(`/api/users/${userId}/notifications`).then(r => r.json()),
  ]);

  return { user, posts, notifications };
}

// Without Promise.all (sequential — 3x slower)
async function loadDashboardSlow(userId) {
  const user = await fetch(`/api/users/${userId}`).then(r => r.json());
  const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
  const notifications = await fetch(`/api/users/${userId}/notifications`).then(r => r.json());
  return { user, posts, notifications };
}
Enter fullscreen mode Exit fullscreen mode

Promise.allSettled()

Like Promise.all(), but waits for all promises to settle (succeed or fail) and never rejects. Returns an array of result objects.

const results = await Promise.allSettled([
  fetch('/api/endpoint-1').then(r => r.json()),
  fetch('/api/endpoint-2').then(r => r.json()),
  fetch('/api/endpoint-that-might-fail').then(r => r.json()),
]);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`Request ${index} succeeded:`, result.value);
  } else {
    console.error(`Request ${index} failed:`, result.reason.message);
  }
});
Enter fullscreen mode Exit fullscreen mode

Promise.race()

Resolves or rejects as soon as the first promise settles. Useful for timeouts.

function fetchWithTimeout(url, timeoutMs) {
  const fetchPromise = fetch(url).then(r => r.json());

  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Request timed out')), timeoutMs)
  );

  return Promise.race([fetchPromise, timeoutPromise]);
}

try {
  const data = await fetchWithTimeout('/api/slow-endpoint', 5000);
  console.log(data);
} catch (err) {
  console.error(err.message); // 'Request timed out' or fetch error
}
Enter fullscreen mode Exit fullscreen mode

Promise.any()

Resolves with the first fulfilled promise (ignores rejections unless all fail). Useful for trying multiple sources.

// Try multiple CDN mirrors, use the first that responds
const data = await Promise.any([
  fetch('https://cdn1.example.com/data.json').then(r => r.json()),
  fetch('https://cdn2.example.com/data.json').then(r => r.json()),
  fetch('https://cdn3.example.com/data.json').then(r => r.json()),
]);
console.log('Got data from fastest source:', data);
Enter fullscreen mode Exit fullscreen mode

Common Patterns and Pitfalls

Avoid awaiting in loops (unless sequential is intentional)

const userIds = [1, 2, 3, 4, 5];

// ❌ Sequential — each request waits for the previous
for (const id of userIds) {
  const user = await fetchUser(id); // waits 1s × 5 = 5s total
  console.log(user.name);
}

// ✅ Parallel — all requests fire at once
const users = await Promise.all(userIds.map(id => fetchUser(id))); // ~1s total
users.forEach(user => console.log(user.name));
Enter fullscreen mode Exit fullscreen mode

Don't forget to await

// ❌ Bug: not awaiting — data will be a Promise, not the value
async function getData() {
  const data = fetchUser(1); // missing await!
  console.log(data); // Promise { <pending> }
  return data;
}

// ✅ Correct
async function getData() {
  const data = await fetchUser(1);
  console.log(data); // { id: 1, name: 'Alice', ... }
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Error handling at the right level

// ❌ Swallowing errors silently
async function bad() {
  try {
    return await riskyOperation();
  } catch (err) {
    // Silent! Caller doesn't know it failed
  }
}

// ✅ Handle what you can, re-throw the rest
async function good() {
  try {
    return await riskyOperation();
  } catch (err) {
    logError(err);          // log for observability
    throw err;              // re-throw so caller can react
  }
}
Enter fullscreen mode Exit fullscreen mode

Using async IIFE when await is needed at the top level

// CommonJS or older environments without top-level await
(async () => {
  try {
    const data = await fetchSomething();
    console.log(data);
  } catch (err) {
    console.error(err);
    process.exit(1);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Promisifying callback-based APIs

const { promisify } = require('util');
const fs = require('fs');

const readFile = promisify(fs.readFile);

// Now you can use it with async/await
async function readConfig() {
  const content = await readFile('./config.json', 'utf8');
  return JSON.parse(content);
}

// Or write your own promisify wrapper
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function main() {
  console.log('Starting...');
  await delay(1000);
  console.log('1 second later');
}
Enter fullscreen mode Exit fullscreen mode

Async Iteration (for await...of)

For async generators and streams, for await...of lets you iterate over async iterables cleanly.

// Reading a stream line by line
async function processLargeFile(filename) {
  const { createReadStream } = require('fs');
  const { createInterface } = require('readline');

  const stream = createReadStream(filename);
  const rl = createInterface({ input: stream });

  for await (const line of rl) {
    processLine(line);
  }
}

// Async generator
async function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    await delay(100); // simulate async work
    yield i;
  }
}

for await (const num of generateSequence(1, 5)) {
  console.log(num); // 1, 2, 3, 4, 5 (with delays)
}
Enter fullscreen mode Exit fullscreen mode

Quick Reference

// Create
new Promise((resolve, reject) => { ... })
Promise.resolve(value)
Promise.reject(error)

// Consume
promise.then(onFulfilled, onRejected)
promise.catch(onRejected)
promise.finally(onFinally)

// Combinators
Promise.all([p1, p2, p3])         // all must succeed
Promise.allSettled([p1, p2, p3])  // wait for all, never rejects
Promise.race([p1, p2, p3])        // first to settle wins
Promise.any([p1, p2, p3])         // first fulfilled wins

// Async/await
async function fn() { ... }       // returns a Promise
const result = await promise;     // pause until resolved
try { await fn() } catch (err) {} // error handling
Enter fullscreen mode Exit fullscreen mode

Master promises and async/await and you'll be able to write clean, efficient, and maintainable asynchronous code in any JavaScript context — browser, Node.js, or serverless functions.

Free Developer Tools

If you found this article helpful, check out DevToolkit — 40+ free browser-based developer tools with no signup required.

Popular tools: JSON Formatter · Regex Tester · JWT Decoder · Base64 Encoder

🛒 Get the DevToolkit Starter Kit on Gumroad — source code, deployment guide, and customization templates.

Top comments (0)