home/tutorials/javascript-promises-async-await
JavaScriptIntermediate

JavaScript Promises & Async/Await Explained

12 min readMar 15, 2025

If you've ever been confused by callbacks, then confused again by Promises, then confused once more by async/await — this guide is for you. We'll build up from scratch, with real examples.

1. The Problem: Asynchronous Code

JavaScript runs in a single thread. That means if you make a network request, the browser can't just sit and wait — it would freeze the page. Instead, JS uses an event loop to handle async operations.

The old way to handle this was callbacks — functions you pass into other functions to call when the work is done. This worked, but led to "callback hell."

callback-hell.js
// The nightmare of nested callbacks
fetchUser(userId, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      // And it keeps going...
      console.log(comments)
    })
  })
})

2. Promises: The Solution

A Promise is an object that represents a value that will be available in the future. It has three states: pending, fulfilled, or rejected.

promises.js
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true
  
  if (success) {
    resolve('Data fetched!')   // ✅ Fulfilled
  } else {
    reject('Something failed') // ❌ Rejected
  }
})

// Consuming a Promise
myPromise
  .then(data => console.log(data))    // 'Data fetched!'
  .catch(err => console.error(err))
  .finally(() => console.log('Done'))

3. Async/Await: Promises Made Beautiful

async/await is just syntax sugar on top of Promises. It makes async code look and behave more like synchronous code — much easier to read and debug.

async-await.js
// Mark function as async
async function fetchUserData(userId) {
  try {
    // 'await' pauses here until Promise resolves
    const response = await fetch(`/api/users/${userId}`)
    const user = await response.json()
    
    const posts = await fetch(`/api/posts?userId=${user.id}`)
    const postsData = await posts.json()
    
    return { user, posts: postsData }
  } catch (error) {
    // Catches both network errors & rejected promises
    console.error('Failed:', error)
    throw error
  }
}

// Usage (async functions return a Promise)
fetchUserData(1).then(data => console.log(data))

💡 Key Rule: You can only use await inside an async function. Using it outside will throw a syntax error (unless you're at the top level of a module).

4. Common Patterns

Running Promises in Parallel

// ❌ SLOW: awaiting sequentially
const user = await fetchUser(1)   // wait 500ms
const posts = await fetchPosts(1) // wait 500ms
// Total: 1000ms

// ✅ FAST: running in parallel
const [user, posts] = await Promise.all([
  fetchUser(1),
  fetchPosts(1)
])
// Total: ~500ms (whichever takes longer)

📌 Quick Summary

  • Callbacks were the original way to handle async code — but they nest and become hard to read.
  • Promises represent a future value. They chain with .then() and .catch().
  • async/await is syntactic sugar over Promises — same thing, nicer syntax.
  • Always wrap await in try/catch to handle errors.
  • Use Promise.all() to run multiple async operations simultaneously.