The Problem: Async Operations

JavaScript is single-threaded. Operations like network requests, file reads, and timers take time — you cannot block the thread waiting for them. You need a way to say "do this, and when it's done, run this code." Here is the same operation implemented with all three patterns:

Pattern 1: Callbacks

A callback is a function passed as an argument to another function, to be called when the async operation completes.

Callbacks — the original pattern function fetchUser(id, callback) { setTimeout(() => { callback(null, { id, name: 'Alice' }); // Node-style: (error, result) }, 100); } function fetchPosts(userId, callback) { setTimeout(() => { callback(null, [{ id: 1, title: 'First Post' }]); }, 100); } // Using the callbacks: fetchUser(1, function(err, user) { if (err) return console.error(err); fetchPosts(user.id, function(err, posts) { if (err) return console.error(err); fetchComments(posts[0].id, function(err, comments) { if (err) return console.error(err); // Callback hell — deeply nested, hard to read console.log(comments); }); }); });

Callback Hell Problems

Deep nesting makes code hard to read ("Pyramid of Doom"). Error handling must be repeated in every callback. Sequential operations require deep nesting. Parallel operations are complex to coordinate. This is why Promises were introduced in ES6.

Pattern 2: Promises

A Promise represents a future value. It can be in one of three states: pending, fulfilled, or rejected.

Promise states and creation // Creating a Promise: const myPromise = new Promise((resolve, reject) => { setTimeout(() => { const success = true; if (success) { resolve({ id: 1, name: 'Alice' }); // fulfilled } else { reject(new Error('Fetch failed')); // rejected } }, 100); }); // Consuming a Promise: myPromise .then(user => { console.log(user.name); // fulfilled handler return user.id; // pass value to next .then() }) .then(id => { return fetchPosts(id); // return another Promise }) .then(posts => { console.log(posts); }) .catch(err => { console.error(err); // ONE catch handles ALL errors above }) .finally(() => { console.log('Done — always runs'); });

Promise Chaining vs Callback Hell

Same operation — Promises vs callbacks // Callbacks (nested): getUser(1, (err, user) => { getPosts(user.id, (err, posts) => { getComments(posts[0].id, (err, comments) => { // 3 levels deep }); }); }); // Promises (flat chain): getUser(1) .then(user => getPosts(user.id)) .then(posts => getComments(posts[0].id)) .then(comments => console.log(comments)) .catch(err => console.error(err));

Promise Static Methods

MethodBehaviourUse Case
Promise.all([])Resolves when ALL resolve; rejects on first rejectionParallel ops where all must succeed
Promise.allSettled([])Waits for all to settle; never rejectsWant all results, even if some fail
Promise.race([])Resolves/rejects when FIRST settlesTimeout patterns
Promise.any([])Resolves when FIRST fulfils; rejects if all rejectFirst successful response wins
Promise.resolve(v)Returns a fulfilled Promise with value vWrap a value in a Promise
Promise.reject(e)Returns a rejected Promise with error eTesting error paths
Promise.all — parallel execution // Sequential (slow — each waits for previous): const user = await fetchUser(1); // 200ms const settings = await fetchSettings(1); // 200ms // Total: 400ms // Parallel (fast — start all at once): const [user, settings] = await Promise.all([ fetchUser(1), // starts immediately fetchSettings(1), // starts immediately ]); // Total: ~200ms (limited by the slowest)

Pattern 3: async/await

async/await was introduced in ES2017 and is now the standard for writing async code. It is built on top of Promises — every async function returns a Promise.

async/await — synchronous-looking code // Declare function as async: async function getUserData(userId) { try { const user = await fetchUser(userId); // await suspends until resolved const posts = await fetchPosts(user.id); // runs after user is fetched const comments = await fetchComments(posts[0].id); return { user, posts, comments }; } catch (err) { console.error('Failed:', err.message); throw err; // re-throw to let callers handle it } } // Use it: const data = await getUserData(1); console.log(data.user.name);

Error Handling Patterns

Multiple error handling approaches // Pattern 1: try/catch (most common) async function fetchData() { try { const res = await fetch('/api/data'); return await res.json(); } catch (err) { console.error(err); return null; } } // Pattern 2: .catch() on the await expression async function fetchData2() { const res = await fetch('/api/data').catch(() => null); if (!res) return null; return res.json(); } // Pattern 3: Separate error handling utility const [error, data] = await fetchData() .then(data => [null, data]) .catch(err => [err, null]); if (error) console.error(error);

Common async/await Mistake: Forgetting await

If you forget await, you get a Promise object instead of the resolved value. This is a common silent bug: const data = fetchUser(1); — data is a Promise, not a user object. TypeScript catches this; plain JavaScript will not warn you. Always be explicit with await.

Comparison: Callbacks vs Promises vs async/await

PropertyCallbacksPromisesasync/await
ReadabilityPoor (nested)Good (flat chain)Excellent (sync-like)
Error handlingPer-callback (tedious)One .catch()try/catch
Parallel opsManual coordinationPromise.all()await Promise.all()
DebuggingStack traces are poorGoodExcellent — full stack traces
Browser supportAll browsersES6+ / IE polyfillES2017+ / Babel
Use today?Only for APIs that require itFor combinators (Promise.all)Default choice

Best Practice: Mix async/await with Promise.all

Use async/await for sequential logic and readability. Use Promise.all() when you need to run multiple async operations in parallel. The two work perfectly together: const [a, b] = await Promise.all([fetchA(), fetchB()]);

How We Research and Update This Guide

We test the underlying formula or workflow, compare outputs with reliable references, and revise examples whenever the page content changes.

  • The workflow or formula is tested directly in the tool and compared against independent reference examples.
  • Examples are kept practical so readers can verify the result without hidden assumptions.
  • Pages are revised whenever the interface, calculation flow, or surrounding guidance materially changes.

Frequently Asked Questions — JavaScript Promises and Async/Await