Promises vs Async/Await in JavaScript: What Is the Difference?

Blog / Promises vs Async/Await in JavaScript: What Is the Difference?
What actually is the difference between promises and async/await in JavaScript? And when should you use one versus the other?

The Callback Hell Problem

Imagine two functions that fetch data from a server. getUser looks up a user by their ID, and getOrders looks up that user's orders by the returned userId. Both take time, so we can't get the result straight away.There's no real server here, so we fake the network latency with setTimeout. Each function waits one second before replying, just like a real request over the network would. When the data is ready, it calls the callback it was given.Before promises existed, asynchronous JavaScript relied entirely on callbacks. You would pass a function to run after an async step finished. But if you had multiple steps that depended on each other, you had to pass a callback inside a callback.
javascript
function getUser(userId, callback) { setTimeout(() => { callback({ id: userId, name: "Sam" }) }, 1000) // fake 1 second network latency } function getOrders(userId, callback) { setTimeout(() => { callback([{ id: 1, item: "Laptop" }]) }, 1000) // fake 1 second network latency } getUser(1, function(user) { getOrders(user.id, function(orders) { console.log(orders) // [{ id: 1, item: "Laptop" }] }) })
When you build complex features, this pattern quickly becomes difficult to read. Your code nests deeper and deeper with every new step. You also have to handle errors manually at every single level.
Each callback sits one level deeper than the last, and every level checks its own errors
Let's walk through the code line by line.
  • Step 1: We call getUser(1) and pass it a callback. It starts a one second timer and returns straight away, so nothing is printed yet.
  • Step 2: After one second, the timer fires and getUser calls our callback with the user, { id: 1, name: "Sam" }.
  • Step 3: Inside that callback, we call getOrders(user.id) and pass it a second callback. This has to go inside the first callback, because it needs the user's ID. Another one second timer starts.
  • Step 4: After another second, getOrders calls the inner callback with the user's orders.
  • Step 5: Inside the inner callback, console.log prints [{ id: 1, item: "Laptop" }], about two seconds after we started.
Every new step needs the result of the one before, so it goes one level deeper. So how did JavaScript solve this messy nesting? This is where promises come in.

How Promises Work

A promise is simply an object representing a value that is not ready yet.A promise can be in one of three states:
  • Pending
  • Fulfilled
  • Rejected
Instead of taking a callback, each function now returns a promise. When the data is ready, it calls resolve with the result, which moves the promise from pending to fulfilled.
javascript
function getUser(userId) { return new Promise(resolve => { setTimeout(() => { resolve({ id: userId, name: "Sam" }) }, 1000) // fake 1 second network latency }) } function getOrders(userId) { return new Promise(resolve => { setTimeout(() => { resolve([{ id: 1, item: "Laptop" }]) }, 1000) // fake 1 second network latency }) }
Instead of nesting callbacks, you chain your steps together using .then(). If any step fails, you handle the error just once by adding a .catch() at the end of the chain.
javascript
getUser(1) .then(user => getOrders(user.id)) .then(orders => console.log(orders)) // [{ id: 1, item: "Laptop" }] .catch(error => console.log(error))
This flattens the code into one chain with one error handler.
One flat chain of steps joined by .then, with a single .catch for an error in any of them
But chaining multiple .then() methods can still feel unnatural compared to standard synchronous code. So how can we make asynchronous code read like normal code? This is the problem async/await solves.

How Async/Await Works

The JavaScript async and await keywords are built directly on top of promises. They are not a replacement for them. Every time you define an async function, it automatically returns a promise.Inside that function, the await keyword pauses execution until the promise settles. It only pauses the specific async function it is inside, without blocking the rest of the program. Errors are handled using a standard try and catch block.
javascript
async function fetchOrders(userId) { try { const user = await getUser(userId) const orders = await getOrders(user.id) console.log(orders) // [{ id: 1, item: "Laptop" }] } catch (error) { console.log(error) } } fetchOrders(1)
It uses the same promise-returning getUser and getOrders. Now the exact same task reads from top to bottom like normal synchronous code.

When to Use Promises

You should use raw promises for short, one-off async chains.They are also the right choice when you need to run independent tasks at the exact same time. You can pass multiple promises to Promise.all and run them concurrently.

When to Use Async/Await

You should use async/await for step-by-step logic where each step needs the result of the previous one. It makes complex logic much easier to read.However, you have to be careful with independent tasks. If you await independent tasks one after another, they will run one at a time. This is much slower than using Promise.all.

Predict the Output

Here's a common JavaScript interview question. Predict the exact output order of this short snippet.
javascript
console.log("1. Start") setTimeout(() => { console.log("2. Timer") }, 0) Promise.resolve().then(() => { console.log("3. Promise") }) console.log("4. End")
Here is the answer:
  • Synchronous code first: synchronous code always runs first, so Start and End print immediately.
  • Then the microtask queue: the event loop checks the microtask queue, which holds promise callbacks. So Promise prints next.
  • Then the macrotask queue: finally, the event loop checks the macrotask queue for timers, printing Timer last.
The console prints Start, End, Promise, then Timer

Recap

FeaturePromisesAsync/Await
Syntax.then() chainsReads top to bottom
Error handling.catch() methodtry/catch block
Best forConcurrent tasksSequential tasks
Promises chain operations using .then() and catch errors with a .catch() method. They are perfect for running multiple tasks concurrently.Async/await lets you write async JavaScript top to bottom and handle errors with try and catch blocks. It is best used for sequential, dependent steps.

About TechPrep

Never walk into a technical interview unprepared again. TechPrep empowers software engineers to stop guessing and start getting offers. We provide the exact questions asked by tech companies across Data Structures & Algorithms, System Design, Low-Level Design & AI coding rounds. Don't leave your career up to chance. Join thousands of engineers who have successfully navigated the tech hiring maze and landed roles at top tech companies.