JavaScript Promises & Async/Await Explained
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."
// 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.
// 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.
// 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.