Munsif.
AboutExperienceProjectsAchievementsBlogsContact
HomeAboutExperienceProjectsAchievementsBlogsContact
Munsif.

Frontend Developer crafting scalable web applications with modern technologies and clean code practices.

Quick Links

  • About
  • Experience
  • Projects
  • Achievements

Connect

© 2026 Shaik Munsif. All rights reserved.

Built with Next.js & Tailwind

0%
Welcome back!Continue where you left off
Back to Blogs
JavaScript

JavaScript Promises & Promise Chaining Mastery: From Callbacks to Async Flow

Escape callback hell! Master JavaScript Promises with comprehensive coverage of creation, chaining, error handling, and static methods like Promise.all() and Promise.race().

Jan 31, 202625 min read
JavaScriptPromisesAsync ProgrammingInterview PrepBest Practices

Introduction

You've probably heard that callbacks can lead to "Callback Hell"—deeply nested pyramids of doom that make code hard to read and maintain. Promises were introduced in ES6 (2015) as the elegant solution to this problem.

But Promises are more than just "callbacks with better syntax." They represent a fundamental shift in how we think about asynchronous operations: as values that will exist in the future. Understanding Promises deeply is essential for modern JavaScript development, from frontend frameworks to Node.js backends.

In this comprehensive guide, you'll master Promises from the ground up: how to create them, chain them, handle errors, and leverage powerful static methods like Promise.all() and Promise.race(). By the end, you'll write async code that's clean, maintainable, and production-ready.


What is a Promise?

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Think of it as a receipt you get when ordering food at a restaurant:

  • Ordering = Creating the Promise
  • Receipt = The Promise object itself
  • Pending = Kitchen is cooking (operation in progress)
  • Fulfilled = Food is ready (success)
  • Rejected = Kitchen is out of ingredients (failure)
const orderFood = new Promise((resolve, reject) => {
    const foodReady = true;
    
    setTimeout(() => {
        if (foodReady) {
            resolve('🍕 Pizza is ready!');
        } else {
            reject('❌ Sorry, we ran out of dough');
        }
    }, 2000);
});

orderFood
    .then(result => console.log(result))    // "🍕 Pizza is ready!"
    .catch(error => console.log(error));

The Three States of a Promise

Every Promise exists in exactly one of three states:

StateDescriptionCan Transition To
PendingInitial state, operation in progressFulfilled or Rejected
FulfilledOperation completed successfully(terminal state)
RejectedOperation failed with an error(terminal state)
💜 important

[!IMPORTANT] Once a Promise is fulfilled or rejected, it's settled and cannot change states. This is a key difference from callbacks, which can be called multiple times.


Creating Promises

The Promise Constructor

const myPromise = new Promise((resolve, reject) => {
    // Asynchronous operation
    const success = true;
    
    if (success) {
        resolve('Operation successful!'); // Fulfill the promise
    } else {
        reject('Operation failed!');      // Reject the promise
    }
});

Key Points:

  1. The constructor takes an executor function with two parameters: resolve and reject
  2. Call resolve(value) when the operation succeeds
  3. Call reject(reason) when the operation fails
  4. The executor runs immediately (synchronously)

Example: Promisifying setTimeout

function delay(ms) {
    return new Promise(resolve => {
        setTimeout(resolve, ms);
    });
}

// Usage
console.log('Starting...');
delay(2000)
    .then(() => console.log('2 seconds later...'));

Example: Simulating an API Call

function fetchUser(userId) {
    return new Promise((resolve, reject) => {
        console.log(`Fetching user ${userId}...`);
        
        setTimeout(() => {
            const users = {
                1: { id: 1, name: 'Alice' },
                2: { id: 2, name: 'Bob' }
            };
            
            const user = users[userId];
            
            if (user) {
                resolve(user);
            } else {
                reject(new Error(`User ${userId} not found`));
            }
        }, 1000);
    });
}

// Usage
fetchUser(1)
    .then(user => console.log('User:', user))
    .catch(error => console.error('Error:', error.message));

Consuming Promises: .then(), .catch(), .finally()

.then() - Handling Success

promise.then(onFulfilled, onRejected);

The .then() method takes up to two callbacks:

  1. onFulfilled - Called when the promise is fulfilled
  2. onRejected - Optional, called when the promise is rejected
fetchUser(1)
    .then(
        user => console.log('Success:', user),
        error => console.error('Failed:', error)
    );

.catch() - Handling Errors

promise.catch(onRejected);

.catch() is shorthand for .then(null, onRejected):

fetchUser(999)
    .then(user => console.log('Success:', user))
    .catch(error => console.error('Error:', error.message));

.finally() - Always Runs

promise.finally(onFinally);

.finally() runs regardless of whether the promise was fulfilled or rejected:

let isLoading = true;

fetchUser(1)
    .then(user => console.log('User:', user))
    .catch(error => console.error('Error:', error))
    .finally(() => {
        isLoading = false;
        console.log('Loading complete');
    });

Promise Chaining: The Power of Promises

The real power of Promises comes from chaining—each .then() returns a new Promise, allowing you to chain operations vertically instead of nesting them.

Basic Chaining

fetchUser(1)
    .then(user => {
        console.log('User:', user.name);
        return user.id; // Return value becomes next promise's value
    })
    .then(userId => {
        console.log('User ID:', userId);
        return userId * 2;
    })
    .then(doubled => {
        console.log('Doubled:', doubled);
    });

Returning Promises in Chains

When you return a Promise from .then(), the next .then() waits for it:

function fetchPosts(userId) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve([
                { id: 1, title: 'Post 1' },
                { id: 2, title: 'Post 2' }
            ]);
        }, 1000);
    });
}

fetchUser(1)
    .then(user => {
        console.log('User:', user.name);
        return fetchPosts(user.id); // Return a new Promise
    })
    .then(posts => {
        console.log('Posts:', posts);
    });

Error Propagation

Errors propagate down the chain until caught:

fetchUser(1)
    .then(user => {
        console.log(user.name);
        throw new Error('Something went wrong!');
    })
    .then(user => {
        console.log('This will NOT run');
    })
    .catch(error => {
        console.error('Caught:', error.message);
    });

Recovering from Errors

After a .catch(), you can continue the chain:

fetchUser(999)
    .then(user => user.name)
    .catch(error => {
        console.log('Using default user');
        return { name: 'Guest' }; // Recover with default
    })
    .then(user => {
        console.log('User name:', user.name); // "Guest"
    });

Promise Static Methods

JavaScript provides powerful static methods on the Promise object for working with multiple promises.

Promise.all() - Wait for All

Waits for all promises to fulfill, or rejects if any promise rejects.

Promise.all([promise1, promise2, promise3])
    .then(results => {
        // results is an array of all fulfilled values
    })
    .catch(error => {
        // First rejection
    });

Example:

const fetchUser1 = fetchUser(1);
const fetchUser2 = fetchUser(2);
const fetchUser3 = fetchUser(3);

Promise.all([fetchUser1, fetchUser2, fetchUser3])
    .then(users => {
        console.log('All users:', users);
    })
    .catch(error => {
        console.error('At least one failed:', error);
    });

Use Case: Loading multiple resources before rendering a page.

Critical Behavior:

  • ✅ If all promises fulfill → Array of results in order
  • ❌ If any promise rejects → Immediately rejects with that error

Promise.allSettled() - Wait for All (Always)

Waits for all promises to settle (fulfill or reject), never rejects.

Promise.allSettled([promise1, promise2, promise3])
    .then(results => {
        // results is an array of objects with status and value/reason
    });

Example:

const promises = [
    fetchUser(1),
    fetchUser(999), // This will fail
    fetchUser(2)
];

Promise.allSettled(promises)
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`Promise ${index}: Success -`, result.value);
            } else {
                console.log(`Promise ${index}: Failed -`, result.reason.message);
            }
        });
    });

// Output:
// Promise 0: Success - { id: 1, name: 'Alice' }
// Promise 1: Failed - User 999 not found
// Promise 2: Success - { id: 2, name: 'Bob' }

Use Case: When you need to know the result of all operations, regardless of success/failure.


Promise.race() - First to Finish

Settles as soon as any promise settles (fulfills or rejects).

Promise.race([promise1, promise2, promise3])
    .then(result => {
        // result of the first promise to settle
    });

Example: Timeout Pattern

function timeout(ms) {
    return new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Timeout!')), ms);
    });
}

Promise.race([
    fetchUser(1),
    timeout(2000)
])
    .then(user => console.log('User:', user))
    .catch(error => console.error('Error:', error.message));

Use Case: Implementing timeouts for slow operations.


Promise.any() - First Fulfillment

Fulfills as soon as any promise fulfills, rejects only if all promises reject.

Promise.any([promise1, promise2, promise3])
    .then(result => {
        // First fulfilled value
    })
    .catch(error => {
        // AggregateError if all rejected
    });

Example:

const servers = [
    fetch('https://server1.com/api'),
    fetch('https://server2.com/api'),
    fetch('https://server3.com/api')
];

Promise.any(servers)
    .then(response => response.json())
    .then(data => console.log('First server response:', data))
    .catch(error => console.error('All servers failed'));

Use Case: Fetching from the fastest available server.


Comparison Table

MethodReturns WhenRejects WhenResult
Promise.all()All fulfillAny rejectsArray of values
Promise.allSettled()All settleNeverArray of {status, value/reason}
Promise.race()First settlesFirst rejectsFirst settled value
Promise.any()First fulfillsAll rejectFirst fulfilled value

Common Anti-Patterns

❌ Anti-Pattern 1: Nesting Promises (Promise Hell)

// BAD: Nesting defeats the purpose of Promises
fetchUser(1)
    .then(user => {
        fetchPosts(user.id)
            .then(posts => {
                console.log(posts);
            });
    });

✅ Fix: Chain Properly

// GOOD: Flat chain
fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => console.log(posts));

❌ Anti-Pattern 2: Forgetting to Return

// BAD: Not returning the promise
fetchUser(1)
    .then(user => {
        fetchPosts(user.id); // ❌ Forgot to return!
    })
    .then(posts => {
        console.log(posts); // undefined
    });

✅ Fix: Always Return

// GOOD: Return the promise
fetchUser(1)
    .then(user => {
        return fetchPosts(user.id); // ✅ Return
    })
    .then(posts => {
        console.log(posts); // Works!
    });

❌ Anti-Pattern 3: Not Handling Rejections

// BAD: Unhandled promise rejection
fetchUser(999)
    .then(user => console.log(user));
// ⚠️ UnhandledPromiseRejectionWarning

✅ Fix: Always Catch

// GOOD: Always have a .catch()
fetchUser(999)
    .then(user => console.log(user))
    .catch(error => console.error(error));

❌ Anti-Pattern 4: Creating Unnecessary Promises

// BAD: Wrapping a promise in a promise
function getUser(id) {
    return new Promise((resolve, reject) => {
        fetchUser(id)
            .then(user => resolve(user))
            .catch(error => reject(error));
    });
}

✅ Fix: Just Return the Promise

// GOOD: Return directly
function getUser(id) {
    return fetchUser(id);
}

Converting Callbacks to Promises

Manual Conversion

// Old callback-based function
function readFileCallback(path, callback) {
    // ... reads file ...
    callback(error, data);
}

// Convert to Promise
function readFilePromise(path) {
    return new Promise((resolve, reject) => {
        readFileCallback(path, (error, data) => {
            if (error) {
                reject(error);
            } else {
                resolve(data);
            }
        });
    });
}

Using util.promisify (Node.js)

const { promisify } = require('util');
const fs = require('fs');

const readFile = promisify(fs.readFile);

readFile('./file.txt', 'utf8')
    .then(data => console.log(data))
    .catch(error => console.error(error));

Promises vs Async/Await

Promises and async/await solve the same problem, but async/await provides cleaner syntax:

Promise Chain

fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => console.log(posts))
    .catch(error => console.error(error));

Async/Await (Same Logic)

async function getUserPosts() {
    try {
        const user = await fetchUser(1);
        const posts = await fetchPosts(user.id);
        console.log(posts);
    } catch (error) {
        console.error(error);
    }
}

getUserPosts();
ℹ️ note

[!NOTE] Under the hood, async/await is syntactic sugar for Promises. Understanding Promises is essential even when using async/await.


5 Interview Questions You MUST Know

1. What are the three states of a Promise?

Answer:

  • Pending: Initial state, operation in progress
  • Fulfilled: Operation completed successfully
  • Rejected: Operation failed Once settled (fulfilled or rejected), a Promise's state cannot change.

2. What's the difference between Promise.all() and Promise.allSettled()?

Answer:

  • Promise.all() rejects immediately if any promise rejects. Returns an array of values if all fulfill.
  • Promise.allSettled() never rejects. It waits for all promises to settle and returns an array of objects describing each result {status, value/reason}.

3. What happens if you don't return a value in a .then() callback?

Answer: If you don't return anything, the next .then() receives undefined. If you're chaining promises, you must return the promise to wait for it.

4. How do you handle errors in a Promise chain?

Answer: Use .catch() at the end of the chain. Errors propagate down until caught. You can also recover from errors by returning a value in .catch().

5. What's the output of this code?

Promise.resolve('A')
    .then(result => {
        console.log(result);
        return 'B';
    })
    .then(result => {
        console.log(result);
    });
console.log('C');

Answer:

C
A
B

The synchronous console.log('C') runs first. Promise callbacks (.then()) are microtasks and run after the current synchronous code completes.


Conclusion

Promises revolutionized asynchronous JavaScript by providing:

  • Predictable flow control through chaining
  • Better error handling with .catch()
  • Composability with static methods like Promise.all()
  • Foundation for async/await syntax

Mastering Promises means:

  1. Understanding the three states (pending, fulfilled, rejected)
  2. Properly chaining with .then()
  3. Always handling errors with .catch()
  4. Knowing when to use Promise.all() vs allSettled() vs race() vs any()
  5. Avoiding anti-patterns like nesting or forgetting to return

Whether you use Promises directly or async/await syntax, understanding how Promises work under the hood is essential for writing robust, maintainable asynchronous code.

The next time you need to handle async operations, reach for Promises—they're the modern, elegant solution to callback hell!

🧠 Test Your Knowledge

Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.

Written by

Shaik Munsif

Read more articles

Found this helpful? Share it with your network!

Question 1 of 10Easy
Score: 0/0

What are the three states of a Promise?