Episode 4 of 5

Promises

Master JavaScript Promises — create, chain, and handle errors with .then(), .catch(), and .finally() to write clean asynchronous code.

Promises

Promises were introduced in ES6 (2015) to solve the problems of callback hell. A Promise represents a value that may not be available yet but will be resolved (or rejected) at some point in the future. Instead of nesting callbacks, Promises let you chain .then() calls in a flat, readable sequence.

What Is a Promise?

A Promise is an object that can be in one of three states:

StateMeaningTransition
PendingThe operation has not completed yetInitial state
FulfilledThe operation completed successfullyPending → Fulfilled (via resolve)
RejectedThe operation failedPending → Rejected (via reject)

Once a Promise settles (fulfilled or rejected), it cannot change state again. A fulfilled Promise stays fulfilled; a rejected Promise stays rejected.

Creating a Promise

const myPromise = new Promise(function(resolve, reject) {
    // Simulate an async operation
    setTimeout(function() {
        const success = true;

        if (success) {
            resolve('Operation completed!');
        } else {
            reject('Something went wrong');
        }
    }, 2000);
});

The Promise constructor takes a function with two parameters — resolve and reject. Call resolve(value) when the operation succeeds and reject(reason) when it fails.

Consuming a Promise

myPromise
    .then(function(result) {
        console.log('Success:', result);
    })
    .catch(function(error) {
        console.log('Error:', error);
    });

// Output (after 2 seconds):
// Success: Operation completed!
MethodWhen It Runs
.then(callback)When the Promise is fulfilled — receives the resolved value
.catch(callback)When the Promise is rejected — receives the error reason
.finally(callback)Always runs after settled — no arguments, useful for cleanup

Wrapping AJAX in a Promise

function fetchData(url) {
    return new Promise(function(resolve, reject) {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);

        xhr.onload = function() {
            if (xhr.status === 200) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject('HTTP Error: ' + xhr.status);
            }
        };

        xhr.onerror = function() {
            reject('Network error');
        };

        xhr.send();
    });
}

// Usage:
fetchData('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data))
    .catch(err => console.log(err));

Wrapping an async operation in a Promise converts it from the callback pattern to the Promise pattern. The function returns a Promise instead of accepting a callback parameter.

Chaining Promises

The real power of Promises is chaining. Each .then() returns a new Promise, so you can chain them in a flat sequence instead of nesting:

fetchData('/api/users/1')
    .then(function(user) {
        console.log('Got user:', user.name);
        return fetchData('/api/posts?userId=' + user.id);
    })
    .then(function(posts) {
        console.log('Got posts:', posts.length);
        return fetchData('/api/comments?postId=' + posts[0].id);
    })
    .then(function(comments) {
        console.log('Got comments:', comments.length);
    })
    .catch(function(error) {
        console.log('Error at any step:', error);
    });

Callbacks vs Promises — Side by Side

// CALLBACK HELL
fetchUser(1, function(err, user) {
    if (err) return console.log(err);
    fetchPosts(user.id, function(err, posts) {
        if (err) return console.log(err);
        fetchComments(posts[0].id, function(err, comments) {
            if (err) return console.log(err);
            console.log(comments);
        });
    });
});

// PROMISE CHAIN
fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => fetchComments(posts[0].id))
    .then(comments => console.log(comments))
    .catch(err => console.log(err));

The Promise version is flat, readable, and has a single .catch() that handles errors from any step in the chain.

Error Handling in Chains

A single .catch() at the end catches errors from any .then() in the chain. If any step rejects or throws, execution jumps to the nearest .catch():

fetchData('/api/users/999')    // User doesn't exist → 404
    .then(user => {
        console.log('This never runs');
        return fetchData('/api/posts');
    })
    .then(posts => {
        console.log('This also never runs');
    })
    .catch(err => {
        console.log('Caught:', err);  // "HTTP Error: 404"
    })
    .finally(() => {
        console.log('Cleanup: always runs');
    });

Promise.all — Parallel Execution

const p1 = fetchData('/api/users/1');
const p2 = fetchData('/api/users/2');
const p3 = fetchData('/api/users/3');

Promise.all([p1, p2, p3])
    .then(function(results) {
        console.log('User 1:', results[0].name);
        console.log('User 2:', results[1].name);
        console.log('User 3:', results[2].name);
    })
    .catch(function(err) {
        console.log('One of the requests failed:', err);
    });

Promise.all() takes an array of Promises and runs them all in parallel. It resolves when all of them succeed (returning an array of results) or rejects as soon as any one of them fails.

Promise.race — First to Settle

const fast = new Promise(resolve => setTimeout(() => resolve('Fast'), 1000));
const slow = new Promise(resolve => setTimeout(() => resolve('Slow'), 3000));

Promise.race([fast, slow])
    .then(result => console.log(result));
// Output: "Fast" (after 1 second)

Promise.race() resolves or rejects with the result of whichever Promise settles first. It is useful for implementing timeouts.

Promise Utility Methods

MethodResolves WhenRejects When
Promise.all()All Promises fulfillAny one rejects
Promise.race()First Promise settles (win or lose)First Promise rejects
Promise.allSettled()All Promises settle (regardless of outcome)Never rejects
Promise.any()First Promise fulfillsAll Promises reject

Common Mistakes

Forgetting to return in .then()

// WRONG — the chain breaks
fetchUser(1)
    .then(user => {
        fetchPosts(user.id);  // Missing return!
    })
    .then(posts => {
        console.log(posts);   // undefined!
    });

// CORRECT — return the Promise
fetchUser(1)
    .then(user => {
        return fetchPosts(user.id);  // Return!
    })
    .then(posts => {
        console.log(posts);   // Works!
    });

Nesting Promises (defeating the purpose)

// WRONG — this is just callback hell with Promises
fetchUser(1).then(user => {
    fetchPosts(user.id).then(posts => {
        fetchComments(posts[0].id).then(comments => {
            console.log(comments);
        });
    });
});

// CORRECT — flat chain
fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => fetchComments(posts[0].id))
    .then(comments => console.log(comments));

Key Takeaways

  • A Promise represents a future value that will be either fulfilled or rejected
  • Use .then() for success, .catch() for errors, and .finally() for cleanup
  • Each .then() returns a new Promise, enabling flat, readable chains
  • A single .catch() at the end handles errors from any step in the chain
  • Promise.all() runs multiple Promises in parallel and waits for all to complete
  • Always return inside .then() to keep the chain connected
  • Promises solve callback hell by replacing deep nesting with flat chains