Problem Solved

JavaScript · promises vs async

Home › Courses › JavaScript › Problems › Promises vs async
🟨 Concept · 4 min read

Promises vs async/await.
Same mechanism, different syntax.

async/await isn't a replacement for promises — it's syntactic sugar on top of them. An async function always returns a promise. await pauses the function until the promise resolves. The underlying mechanism is identical. What changes is readability and ergonomics. This page covers when each shines, how to run promises in parallel, and the most common async/await mistake: treating the return value as synchronous.

Short answer: Use async/await for sequential operations and try/catch error handling. Use promise methods (Promise.all, Promise.allSettled) for parallel operations. An async function always returns a promise — the return value is the resolved value, not the value itself. If you need to run multiple async operations at the same time, await Promise.all([...]) — not sequential await calls. The syntax is different; the underlying mechanism is the same.

✅ Same thing, two syntaxes

What promises and async/await actually do.

same-thing.js
// Promise syntax
function getUser() {
return fetch('/api/user')
.then(res => res.json())
.then(data => data.user);
}

// async/await syntax — same thing
async function getUserAsync() {
const res = await fetch('/api/user');
const data = await res.json();
return data.user;
}

// Both return a promise.
// Both resolve to the user object.
// Both can be awaited or .then'd.
Under the hood:

async function getUserAsync() { ... }
→ The function is wrapped so that its
return value becomes a resolved promise.
→ Each await becomes a .then() that pauses
the function and resumes when the promise
resolves.
→ The mechanism is identical to the promise
version. Only the syntax is different.

The async keyword does two things:
1. Wraps the return value in a promise.
2. Allows await inside the function.

The await keyword does one thing:
Pauses the function until the promise resolves,
then resumes with the resolved value.

✅ Same behavior, same performance.
✅ async/await is just easier to read.
✅ Promises are still there — you use them
whenever you call await.

✅ Sequential vs parallel

The most common async mistake.

sequential-vs-parallel.js
// ❌ Sequential — each waits for the previous
async function loadSequential() {
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const comments = await fetch('/api/comments').then(r => r.json());
return { user, posts, comments };
}
// Total time = user + posts + comments
// If each is 200ms, total = 600ms

// ✅ Parallel — all run at the same time
async function loadParallel() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { user, posts, comments };
}
// Total time = max(user, posts, comments)
// If each is 200ms, total = 200ms
Why the difference:

Sequential await:
const a = await p1; // wait for p1
const b = await p2; // then wait for p2
const c = await p3; // then wait for p3
→ p2 doesn't start until p1 finishes.
→ Total = p1 + p2 + p3.

Promise.all:
await Promise.all([p1, p2, p3]);
→ All three start at the same time.
→ The await waits for the last one.
→ Total = max(p1, p2, p3).

This is the most common async/await performance
bug: writing sequential awaits when the operations
are independent.

Rule: if the operations don't depend on each other,
use Promise.all. Only await sequentially when
each step needs the previous result.

For error handling with parallel:
try {
const results = await Promise.all([...]);
} catch (err) {
// one failed — all fail
}

For partial results:
const results = await Promise.allSettled([...]);
// results is an array of {status, value/reason}

✅ Error handling

try/catch vs .catch().

error-handling.js
// Promise style
function getUser() {
return fetch('/api/user')
.then(res => res.json())
.then(data => data.user)
.catch(err => {
console.error('Failed:', err);
return null;
});
}

// async/await style
async function getUserAsync() {
try {
const res = await fetch('/api/user');
const data = await res.json();
return data.user;
} catch (err) {
console.error('Failed:', err);
return null;
}
}

// Both work.
// async/await looks like synchronous error handling.
// That's its main advantage.
Why async/await wins for error handling:

Promise chain:
→ .catch() at the end.
→ To catch errors from multiple steps,
one .catch is enough.
→ But if you need per-step error handling,
you need multiple .catch() calls.

async/await:
→ One try/catch block.
→ Catches any await that rejects.
→ Per-step handling is possible with
nested try/catch.
→ Looks like synchronous code.

The mistake: forgetting to catch.

❌ async function foo() {
await fetch('/api'); // no try/catch
}
→ Unhandled rejection if fetch fails.

✅ async function foo() {
try { await fetch('/api'); }
catch (e) { console.error(e); }
}

For top-level async (modules):
→ Unhandled rejections crash the process
in Node, or show an error in the console
in browsers.
→ Always handle rejections, or use a global
unhandledRejection handler.

Don't use await inside a loop. for (const item of items) { await process(item); } runs sequentially — each iteration waits for the previous. If process takes 100ms and you have 10 items, the loop takes 1 second. Use Promise.all(items.map(item => process(item))) instead — all items process in parallel, total time is ~100ms. The same rule as the sequential vs parallel section applies to loops: if the iterations are independent, use Promise.all; if each depends on the previous result, sequential await is correct. The mistake is using sequential await when the operations are independent.

⚠️ Common mistakes

The promises/async mistakes that cost real time.

1. Sequential await for parallel operations

Each waits for the previous. Fix: Promise.all.

2. Treating an async function's return as synchronous

It returns a promise. Fix: await or .then().

3. No try/catch around await

Unhandled rejections. Fix: wrap in try/catch.

4. await inside forEach

forEach doesn't await — the loop finishes before the async work. Fix: for...of with await, or Promise.all.

5. Promise.all with a rejection

One failure fails all. Fix: Promise.allSettled for partial results.

6. Forgetting return in a .then()

The next .then() gets undefined. Fix: always return.

7. Mixing .then() and await in the same function

Confusing and error-prone. Fix: pick one per function.

8. Not knowing when async/await isn't available

Older environments need transpilation. Fix: check target support or use promises.

❓ Frequently asked

What is the difference between promises and async/await?

They're the same mechanism with different syntax. A promise is an object that represents a future value — you attach .then() and .catch() callbacks. async/await is syntactic sugar on top of promises: an async function always returns a promise, and await pauses the function until the promise resolves. Under the hood, async/await still uses promises and microtasks.

Is async/await better than promises?

Neither is universally better — they're tools for different jobs. async/await is better for: sequential operations, try/catch error handling, and reading like synchronous code. Promises are better for: parallel operations (Promise.all), functional pipelines (.then chains), and cases where you need the promise object itself.

How do I run promises in parallel?

Use Promise.all() for parallel execution. It takes an array of promises and returns a single promise that resolves when all of them resolve. Example: const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]). This runs both fetches simultaneously — total time is max(fetchUser, fetchPosts), not the sum. For partial results, use Promise.allSettled().

Why doesn't my async function return the value?

Because async functions always return a promise. If you write async function getUser() { return { name: 'Alice' }; }, calling getUser() returns a promise, not the object. To get the value, you must await it: const user = await getUser(); or use .then(). The return value inside the async function becomes the resolved value of the promise.

How do I handle errors in async/await?

Use try/catch, same as synchronous code. try { const data = await fetchData(); } catch (error) { console.error(error); }. This is one of the main advantages of async/await over promises — you can use the same error handling pattern as synchronous code. With promises, you need .catch() at the end of the chain.

When should I use .then() instead of await?

Three cases. (1) At the top level of a module. (2) When you need to return a promise from a function without making the function async. (3) When you're building a functional pipeline where the promise is passed through transformations. In modern application code, async/await handles 90% of cases; .then() handles the rest.

🔗 Continue learning

🗺️ JavaScript learning path
The full roadmap from variables to async — every concept in order

Next in this series:

⚠️ Async/await error handling
try/catch, unhandled rejections, and the pattern that doesn't lose errors
← All JavaScript problems
Problem solved · JavaScript