JavaScript Promises

Handle asynchronous operations elegantly with .then/.catch chains, and run multiple async tasks in parallel with Promise.all and friends.

Intermediate 11 min read 10 examples

Promise Basics

A Promise is created with the new Promise(executor) constructor. The executor receives two functions: resolve (call with the success value) and reject (call with an Error).

JavaScript
// Creating a Promise
const promise = new Promise(function(resolve, reject) {
    // Simulate async work (e.g., a network request)
    setTimeout(function() {
        const success = true;
        if (success) {
            resolve({ id: 1, name: 'Alice' }); // fulfill with a value
        } else {
            reject(new Error('Failed to fetch user')); // reject with an Error
        }
    }, 1000);
});

// Using the Promise
promise
    .then(user => console.log('User:', user.name)) // 'Alice'
    .catch(err  => console.error('Error:', err.message));

// Promise.resolve / Promise.reject - create already-settled Promises
const resolved = Promise.resolve(42);        // instantly fulfilled
const rejected = Promise.reject(new Error('oops')); // instantly rejected

resolved.then(val => console.log(val)); // 42
rejected.catch(err => console.log(err.message)); // 'oops'

.then, .catch, .finally

Each handler returns a new Promise, which is what enables chaining.

JavaScript
fetchUser(42)
    .then(function(user) {
        // Called if fetchUser resolves
        console.log('Got user:', user.name);
        return user; // passed to next .then
    })
    .catch(function(err) {
        // Called if fetchUser rejects OR if any prior .then throws
        console.error('Something failed:', err.message);
        // return a default to recover, or re-throw to keep failing
        return { name: 'Guest' }; // recover with default
    })
    .finally(function() {
        // Always runs - whether resolved or rejected
        // Great for cleanup: hiding spinners, resetting state
        // Does NOT receive the value - use .then for that
        hideLoadingSpinner();
    });

// .then(onFulfilled, onRejected) - two-argument form (less common)
fetchUser(42).then(
    user => console.log('success:', user.name),
    err  => console.error('error:', err.message)
);
// Difference from .catch: this form does NOT catch errors thrown inside onFulfilled

Chaining Promises

Return a Promise inside .then() to wait for it before calling the next .then(). This creates sequential async steps in a flat chain.

JavaScript
// Sequential async steps - flat chain
getUser(userId)
    .then(user   => getOrders(user.id))    // return a new Promise
    .then(orders => getDetails(orders[0])) // wait for previous
    .then(detail => render(detail))
    .catch(err   => showError(err.message)); // catches any error in the chain

// Transforming values inline (no new Promise needed for sync transforms)
fetch('/api/products')
    .then(response => response.json())      // returns Promise
    .then(data     => data.filter(p => p.inStock))  // sync transform
    .then(inStock  => {
        inStock.forEach(p => renderProduct(p));
        return inStock.length; // pass count to next handler
    })
    .then(count => console.log(`${count} products shown`))
    .catch(err  => console.error(err));

// WRONG - nested .then recreates callback hell
fetch('/api/user').then(res => {
    res.json().then(user => {       // nested - don't do this
        fetch('/api/orders/' + user.id).then(r => {
            // ever deeper nesting...
        });
    });
});

// RIGHT - return the inner Promise so the chain stays flat
fetch('/api/user')
    .then(res  => res.json())
    .then(user => fetch('/api/orders/' + user.id))
    .then(res  => res.json())
    .then(orders => console.log(orders))
    .catch(err => console.error(err));

Promise States

StateDescriptionTransitions to
pendingInitial state - operation in progressfulfilled or rejected
fulfilledOperation completed successfullySettled - cannot change again
rejectedOperation failedSettled - cannot change again
Promises are immutable once settled

Once a Promise resolves or rejects, its state and value are locked forever. Calling resolve() a second time has no effect. This is a key difference from callbacks, which can theoretically be called multiple times.

Promise.all

Run multiple async operations in parallel and wait for all of them to finish. Fails fast - if any promise rejects, the whole thing rejects.

JavaScript
// Run three fetches in parallel - much faster than sequential
Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json()),
]).then(([user, posts, comments]) => {
    // All three resolved - destructure the results array
    console.log(user, posts, comments);
}).catch(err => {
    // Any single rejection causes this to fire
    console.error('At least one request failed:', err);
});

// With async/await (cleaner)
async function loadDashboard() {
    const [user, posts, comments] = await Promise.all([
        fetchUser(),
        fetchPosts(),
        fetchComments(),
    ]);
    render(user, posts, comments);
}

// Dynamic array of promises
const userIds = [1, 2, 3, 4, 5];
const userPromises = userIds.map(id => fetchUser(id));
const users = await Promise.all(userPromises); // array of 5 users

Promise Combinators

JavaScript
// Promise.allSettled - wait for ALL, regardless of failures
// Returns: [{ status: 'fulfilled', value: ... }, { status: 'rejected', reason: ... }]
const results = await Promise.allSettled([
    fetchUser(1),
    fetchUser(2), // might fail
    fetchUser(3),
]);
results.forEach(result => {
    if (result.status === 'fulfilled') {
        console.log('Got user:', result.value.name);
    } else {
        console.error('Failed:', result.reason.message);
    }
});

// Promise.race - resolves/rejects with the FIRST settled Promise
// Use case: timeout pattern
function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Timeout')), ms)
    );
    return Promise.race([promise, timeout]);
}
const user = await withTimeout(fetchUser(1), 5000);

// Promise.any - resolves with the FIRST fulfilled Promise (ignores rejections)
// Rejects with AggregateError only if ALL reject
const fastest = await Promise.any([
    fetch('https://cdn1.example.com/data.json'),
    fetch('https://cdn2.example.com/data.json'),
    fetch('https://cdn3.example.com/data.json'),
]).then(r => r.json());

// Summary: which combinator to use?
// Promise.all        - need ALL results, fail fast if any fails
// Promise.allSettled - need ALL results, handle each failure individually
// Promise.race       - need FIRST settled (fulfilled or rejected)
// Promise.any        - need FIRST fulfilled (timeout if all fail)

Creating Promises

JavaScript
// Wrap setTimeout in a Promise
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(1000); // pause execution for 1 second

// Wrap an event in a Promise
function waitForClick(element) {
    return new Promise(resolve => {
        element.addEventListener('click', resolve, { once: true });
    });
}
const event = await waitForClick(document.querySelector('#btn'));

// Wrap a callback-based API
function readFileAsync(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, 'utf8', (err, data) => {
            if (err) reject(err);
            else resolve(data);
        });
    });
}

// Conditional Promise - sometimes async, sometimes not
function getUser(id) {
    if (cache.has(id)) {
        return Promise.resolve(cache.get(id)); // synchronously resolved
    }
    return fetchUser(id).then(user => {
        cache.set(id, user);
        return user;
    });
}

Common Mistakes

JavaScript
// Mistake 1: Forgetting to return a Promise inside .then()
fetchUser(1)
    .then(user => {
        fetchOrders(user.id); // forgot return! next .then gets undefined
    })
    .then(orders => console.log(orders)); // undefined - bug!

// Fix:
fetchUser(1)
    .then(user => fetchOrders(user.id)) // return the Promise
    .then(orders => console.log(orders));

// Mistake 2: Unhandled rejections
fetchUser(1)
    .then(user => console.log(user));
// No .catch() - rejection is silently swallowed (or causes UnhandledPromiseRejection)

// Fix: always add .catch()
fetchUser(1)
    .then(user => console.log(user))
    .catch(err => console.error(err));

// Mistake 3: Promise constructor anti-pattern
// WRONG - unnecessary Promise wrapping of another Promise
function getUser(id) {
    return new Promise((resolve, reject) => {
        fetch('/api/users/' + id)
            .then(r => r.json())
            .then(resolve)
            .catch(reject);
    });
}

// RIGHT - just return the Promise chain directly
function getUser(id) {
    return fetch('/api/users/' + id).then(r => r.json());
}
Use async/await for cleaner code

Promise chains are powerful but async/await (syntactic sugar over Promises) is often clearer for sequential async code. The next lesson covers async/await in depth. Both compile to the same Promise machinery - choose whichever reads better for the situation.

Frequently Asked Questions

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It is a placeholder for a value that does not exist yet. A Promise is in one of three states: pending (initial state), fulfilled (operation succeeded, value available), or rejected (operation failed, error available). Once settled (fulfilled or rejected), a Promise stays in that state forever.

Promise.all() runs all promises in parallel and resolves with an array of all results - but rejects immediately if ANY promise rejects ("fail fast"). Promise.allSettled() always waits for all promises to complete regardless of failures, and resolves with an array of objects with a status property ("fulfilled" or "rejected"). Use allSettled when you need results from all operations even if some fail.

Nesting .then() inside .then() recreates callback hell with Promises. Instead, return the next Promise from inside a .then() handler - this passes control to the next .then() in the chain at the same nesting level. The chain stays flat, errors propagate to a single .catch() at the end, and the code reads top-to-bottom.

No - .catch() catches any rejection in the preceding chain, including: rejected Promises, errors thrown with throw inside a .then() handler, and runtime errors (like calling a method on undefined) inside a .then() handler. This is because throwing inside a .then() automatically converts the returned Promise to rejected.