Promises

May 18, 2026
#javascript #js #promises #async

JavaScript is single-threaded — it can only do one thing at a time. But web applications need to fetch data from servers, read files, and wait for user input without freezing the page. Promises are how JavaScript handles these asynchronous operations: they represent a value that doesn’t exist yet but will (or won’t) in the future.

The Problem Promises Solve

Before promises, async code used callbacks. This worked for simple cases but quickly became unmanageable:

// "Callback hell" — nested callbacks are hard to read and maintain
getUser(userId, function (user) {
    getOrders(user.id, function (orders) {
        getOrderDetails(orders[0].id, function (details) {
            getShippingStatus(details.trackingId, function (status) {
                console.log(status);
                // Error handling? Good luck.
            });
        });
    });
});

Promises flatten this into a readable chain.

What Is a Promise?

A promise is an object that represents the eventual result of an async operation. It’s in one of three states:

  • Pending — the operation hasn’t completed yet
  • Fulfilled — the operation succeeded, and the promise has a value
  • Rejected — the operation failed, and the promise has a reason (error)

Once a promise is fulfilled or rejected, it’s settled — it can’t change state again.

Using Promises

Most of the time, you’ll consume promises returned by APIs (like fetch), not create them yourself.

.then() and .catch()

fetch("https://api.example.com/users/1")
    .then(response => response.json())
    .then(user => {
        console.log(user.name);
    })
    .catch(error => {
        console.error("Failed to fetch user:", error.message);
    });

.then() runs when the promise fulfills. .catch() runs when it rejects. They return new promises, so you can chain them.

Chaining

Each .then() returns a new promise, so you can chain operations sequentially:

fetch("/api/user")
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return response.json();
    })
    .then(user => fetch(`/api/orders?userId=${user.id}`))
    .then(response => response.json())
    .then(orders => {
        console.log(`User has ${orders.length} orders`);
    })
    .catch(error => {
        // Catches errors from ANY step in the chain
        console.error("Something went wrong:", error.message);
    });

.finally()

Runs regardless of whether the promise fulfilled or rejected — useful for cleanup:

const spinner = document.querySelector(".spinner");
spinner.style.display = "block";

fetch("/api/data")
    .then(response => response.json())
    .then(data => renderData(data))
    .catch(error => showError(error))
    .finally(() => {
        spinner.style.display = "none";  // always hide spinner
    });

Creating Promises

When you need to wrap callback-based code or create your own async operations:

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

delay(2000).then(() => console.log("2 seconds passed"));
function loadImage(url) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = () => reject(new Error(`Failed to load: ${url}`));
        img.src = url;
    });
}

loadImage("/photo.jpg")
    .then(img => document.body.appendChild(img))
    .catch(err => console.error(err.message));

The Promise constructor takes a function with two parameters:

  • resolve(value) — call this when the operation succeeds
  • reject(error) — call this when it fails

Promise Static Methods

Promise.all — wait for all to complete

const urls = ["/api/users", "/api/posts", "/api/comments"];

const requests = urls.map(url => fetch(url).then(r => r.json()));

Promise.all(requests)
    .then(([users, posts, comments]) => {
        console.log(`${users.length} users`);
        console.log(`${posts.length} posts`);
        console.log(`${comments.length} comments`);
    })
    .catch(error => {
        // If ANY request fails, the whole thing rejects
        console.error("One of the requests failed:", error);
    });

Promise.allSettled — wait for all, regardless of success/failure

const requests = [
    fetch("/api/users"),
    fetch("/api/broken-endpoint"),
    fetch("/api/posts")
];

Promise.allSettled(requests).then(results => {
    results.forEach((result, i) => {
        if (result.status === "fulfilled") {
            console.log(`Request ${i} succeeded`);
        } else {
            console.log(`Request ${i} failed: ${result.reason}`);
        }
    });
});

Promise.race — first one to settle wins

// Timeout pattern: race a fetch against a timer
function fetchWithTimeout(url, ms) {
    const timeout = new Promise((_, reject) => {
        setTimeout(() => reject(new Error("Request timed out")), ms);
    });

    return Promise.race([fetch(url), timeout]);
}

fetchWithTimeout("/api/slow-endpoint", 5000)
    .then(response => response.json())
    .catch(error => console.error(error.message));

Promise.any — first one to fulfill wins

// Try multiple CDNs, use whichever responds first
Promise.any([
    fetch("https://cdn1.example.com/data.json"),
    fetch("https://cdn2.example.com/data.json"),
    fetch("https://cdn3.example.com/data.json")
])
    .then(response => response.json())
    .then(data => console.log("Got data from fastest CDN"))
    .catch(error => console.error("All CDNs failed"));

Error Handling Patterns

Always catch errors

An unhandled promise rejection is a bug. Always add .catch() at the end of your chains:

// Bad — unhandled rejection if fetch fails
fetch("/api/data").then(r => r.json()).then(processData);

// Good
fetch("/api/data")
    .then(r => r.json())
    .then(processData)
    .catch(handleError);

Recovering from errors

.catch() returns a new promise, so you can recover and continue the chain:

fetch("/api/user-preferences")
    .then(r => r.json())
    .catch(() => {
        // If preferences fail to load, use defaults
        return { theme: "light", fontSize: 14 };
    })
    .then(prefs => applyPreferences(prefs));

Retrying failed requests

function fetchWithRetry(url, retries = 3) {
    return fetch(url).catch(error => {
        if (retries > 0) {
            console.log(`Retrying... (${retries} attempts left)`);
            return fetchWithRetry(url, retries - 1);
        }
        throw error;
    });
}

Common Mistakes

Forgetting to return in .then()

// Bug — the second .then() receives undefined
fetch("/api/user")
    .then(response => {
        response.json();  // missing return!
    })
    .then(user => {
        console.log(user);  // undefined
    });

// Fix
fetch("/api/user")
    .then(response => response.json())
    .then(user => console.log(user));

Creating promises unnecessarily

// Bad — wrapping an existing promise in a new one
function getUser(id) {
    return new Promise((resolve, reject) => {
        fetch(`/api/users/${id}`)
            .then(r => r.json())
            .then(resolve)
            .catch(reject);
    });
}

// Good — just return the promise chain
function getUser(id) {
    return fetch(`/api/users/${id}`).then(r => r.json());
}

What’s Next

Promises are powerful, but chaining .then() calls can still get verbose. Async/await builds on promises to let you write asynchronous code that looks synchronous — it’s the modern way to handle async operations in JavaScript.