Skip to main content
Back to Articles

JavaScript Promises vs Async/Await: Master Async Code (Complete Guide)

Master JavaScript promises and async/await, parallelize operations, speed up code 3x, handle errors correctly.

March 7, 20267 min readBy Mathematicon

JavaScript Promises vs Async/Await: Master Async Code (Complete Guide)

The Problem You're Solving

Your code processes data sequentially (slow):

// āŒ Slow - Waits 3 seconds total (1+1+1)
const user = getUser(1);      // 1 second
const posts = getPosts(1);    // 1 second
const comments = getComments(1);  // 1 second
// Total: 3 seconds (serial)

// āœ… Fast - Runs in parallel (1 second)
const [user, posts, comments] = await Promise.all([
  getUser(1),
  getPosts(1),
  getComments(1)
]);
// Total: 1 second (parallel)

That difference = 3-second page load vs 1-second page load = 65% faster.

Async mastery appears in 29% of JavaScript interviews and directly impacts app responsiveness.

Promises: Foundation

Problem: Callback Hell

// āŒ Hard to read
getUser(1, function(err, user) {
  if (err) throw err;
  getPosts(user.id, function(err, posts) {
    if (err) throw err;
    getComments(posts[0].id, function(err, comments) {
      if (err) throw err;
      console.log(comments);
    });
  });
});

Solution: Promises

// āœ… Readable
getUser(1)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(err => console.error(err));

Promise States

const promise = new Promise((resolve, reject) => {
  if (success) {
    resolve(value);  // āœ… Fulfilled
  } else {
    reject(error);   // āŒ Rejected
  }
  // Pending until resolved or rejected
});
// States:
// Pending → (do work) → Fulfilled/Rejected (final)

const p1 = new Promise(resolve => resolve('done'));
console.log(p1);  // Promise { 'done' } (fulfilled)

const p2 = new Promise((resolve, reject) => reject('error'));
console.log(p2);  // Promise { <rejected> 'error' }

Chaining: .then()

fetch('/api/user')
  .then(res => res.json())          // Chain operation 1
  .then(user => getPosts(user.id))  // Chain operation 2
  .then(posts => console.log(posts))  // Chain operation 3
  .catch(err => console.error(err));   // Error handling

Parallel: Promise.all()

// āŒ Sequential (3 seconds)
const user = await getUser(1);
const posts = await getPosts(1);
const comments = await getComments(1);

// āœ… Parallel (1 second)
const [user, posts, comments] = await Promise.all([
  getUser(1),
  getPosts(1),
  getComments(1)
]);

// If ANY promise rejects, whole thing fails

Race: Promise.race()

// āœ… Fastest request wins
const result = await Promise.race([
  fetch('api1.example.com'),
  fetch('api2.example.com'),
  fetch('api3.example.com')
]);
// Whichever finishes first

// Use case: Timeout
const timeoutPromise = new Promise((resolve, reject) =>
  setTimeout(() => reject(new Error('timeout')), 5000)
);

const result = await Promise.race([
  fetch('/slow-api'),
  timeoutPromise
]);

Async/Await: Cleaner Syntax

Basic Async/Await

// āŒ Promise chain
function loadUser(id) {
  return fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(user => console.log(user));
}

// āœ… Async/await
async function loadUser(id) {
  const res = await fetch(`/api/users/${id}`);
  const user = await res.json();
  console.log(user);
}

// Call it
await loadUser(1);  // Wait for completion

Error Handling

// āŒ Promise - .catch() required
getUser(1)
  .then(user => getPosts(user.id))
  .catch(err => console.error(err));

// āœ… Async/await - try/catch (familiar pattern)
async function loadUserPosts() {
  try {
    const user = await getUser(1);
    const posts = await getPosts(user.id);
    return posts;
  } catch (err) {
    console.error(err);
    return [];
  }
}

Parallel Operations

// āŒ Sequential (slow)
async function loadAll() {
  const user = await getUser(1);
  const posts = await getPosts(1);
  const comments = await getComments(1);
  return [user, posts, comments];
}

// āœ… Parallel (fast)
async function loadAll() {
  const [user, posts, comments] = await Promise.all([
    getUser(1),
    getPosts(1),
    getComments(1)
  ]);
  return [user, posts, comments];
}

Real-World Examples

Example 1: Form Submission

// āŒ Callback hell
function handleSubmit(formData) {
  validateForm(formData, function(err, result) {
    if (err) {
      showError(err);
      return;
    }
    saveToDatabase(result, function(err, id) {
      if (err) {
        showError(err);
        return;
      }
      redirectToDashboard(id);
    });
  });
}

// āœ… Async/await
async function handleSubmit(formData) {
  try {
    const validated = await validateForm(formData);
    const id = await saveToDatabase(validated);
    redirectToDashboard(id);
  } catch (err) {
    showError(err);
  }
}

Example 2: Retry Logic

// Retry up to 3 times on failure
async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fetch(url);
    } catch (err) {
      if (i === retries - 1) throw err;  // Last attempt failed
      await sleep(1000);  // Wait 1 second before retry
    }
  }
}

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

// Usage
const result = await fetchWithRetry('https://api.example.com');

Example 3: Timeout Wrapper

// Wrap any promise with timeout
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((resolve, reject) =>
      setTimeout(() => reject(new Error('timeout')), ms)
    )
  ]);
}

// Usage
try {
  const result = await withTimeout(fetch('/slow-api'), 5000);
} catch (err) {
  console.error('Request timed out');
}

Common Mistakes

āŒ Mistake 1: Mixing Callbacks and Promises

// WRONG - Mixing patterns
setTimeout(() => {
  getUser(1).then(user => console.log(user));
}, 1000);

// CORRECT - All async
async function loadWithDelay() {
  await sleep(1000);
  const user = await getUser(1);
  console.log(user);
}

āŒ Mistake 2: Forgetting await

// WRONG - Returns Promise, not data
async function getUser() {
  const res = fetch('/api/user');  // Missing await
  return res.json();  // res is Promise, not Response
}

// CORRECT
async function getUser() {
  const res = await fetch('/api/user');
  return res.json();
}

āŒ Mistake 3: Sequential When Parallel Intended

// WRONG - 3 seconds (sequential)
async function loadAll() {
  const user = await getUser();
  const posts = await getPosts();
  const comments = await getComments();
}

// CORRECT - 1 second (parallel)
async function loadAll() {
  return Promise.all([
    getUser(),
    getPosts(),
    getComments()
  ]);
}

āŒ Mistake 4: Forgetting Error Handling

// WRONG - Unhandled promise rejection
async function dangerous() {
  const user = await getUser();  // If rejects, error not caught
  return user;
}

dangerous();  // Crashes silently

// CORRECT
async function safe() {
  try {
    const user = await getUser();
    return user;
  } catch (err) {
    console.error(err);
    return null;
  }
}

Performance Comparison

Sequential (Slow)

async function slow() {
  const a = await task1();  // 1s
  const b = await task2();  // 1s (waits for a)
  const c = await task3();  // 1s (waits for b)
  // Total: 3s
}

Parallel (Fast)

async function fast() {
  const [a, b, c] = await Promise.all([
    task1(),  // 1s
    task2(),  // 1s (runs simultaneously)
    task3()   // 1s (runs simultaneously)
  ]);
  // Total: 1s
}

FAQ: Async Mastery

Q1: Promise vs Async/Await - Which to use?

A: Async/await for new code. Promises for library code.

// āœ… Use async/await - cleaner
async function loadUser() {
  const user = await getUser(1);
  return user;
}

// āœ… Use Promise - library wrapper
function withCache(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    return cache.has(key)
      ? Promise.resolve(cache.get(key))
      : fn(...args).then(result => {
          cache.set(key, result);
          return result;
        });
  };
}

Q2: How do I cancel a promise?

A: Use AbortController.

const controller = new AbortController();

fetch('/slow-api', { signal: controller.signal })
  .then(res => res.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request cancelled');
    }
  });

// Cancel
setTimeout(() => controller.abort(), 5000);

Q3: Interview Question: Implement Promise.all() yourself.

A: Here's how:

function allPromises(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;

    if (promises.length === 0) {
      resolve([]);
      return;
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(result => {
          results[index] = result;
          completed++;
          if (completed === promises.length) {
            resolve(results);  // All done
          }
        })
        .catch(err => {
          reject(err);  // First error rejects all
        });
    });
  });
}

// Test
allPromises([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then(results => console.log(results));  // [1, 2, 3]

Q4: Async/await vs Promises - Performance?

A: Same performance. Async/await is syntactic sugar.

// These are equivalent
async function fn1() {
  return await promise;
}

function fn2() {
  return promise.then(x => x);
}

// Both use same Promise machinery under the hood

Conclusion

Master async patterns and your code will be:

  1. Readable - Async/await looks synchronous
  2. Correct - Proper error handling prevents crashes
  3. Fast - Promise.all for parallel operations
  4. Maintainable - Easy to understand flow

The rule: Use async/await for readability, Promise.all for performance.


Learn More

Share this article

Related Articles