JavascriptPromises

JavaScript Promises Explained: Examples and Best Practices

JavaScript Promises Explained: Examples and Best Practices

Ever called an API and needed the rest of your code to wait until the response came back? That's exactly the problem JavaScript Promises solve. A Promise represents a result that isn't ready yet, which makes asynchronous work far easier to organize, combine, and handle.

You'll find Promises everywhere in modern JavaScript: fetch(), database clients, file operations, and most application frameworks lean on them. Learn how they work and async/await will feel like a natural next step rather than a new topic.

What You'll Learn About JavaScript Promises

This guide walks through the Promise lifecycle, chaining, error handling, runtime behavior, and the patterns you'll actually reach for in production.

Prerequisites for Learning Promises

Promises click faster once you're comfortable with functions, callbacks, and the JavaScript event loop.

Why Do JavaScript Promises Exist?

Promises exist to make asynchronous work easier to manage. Before they arrived, the usual approach was to pass callback functions into other functions. String a few dependent operations together and you ended up with deeply nested code, error handling scattered in every layer, and the tangle people call "callback hell." A Promise replaces all of that with a single object that stands in for a future result.

Without them, you'd still be nesting callbacks for network requests, timers, file operations, and database queries. Coordinating even a handful of operations would mean tracking counters by hand and checking for errors at every step.

Promises introduced a predictable way to:

  • connect asynchronous steps
  • pass results between operations
  • centralize error handling
  • run independent operations together
  • separate the operation from its consumers

One thing they don't do is make asynchronous work finish faster. What they change is how easily you can reason about the result and the flow of control around it.

A Mental Model for JavaScript Promises

The easiest way to picture a Promise is as a numbered token for work that hasn't finished yet.

The key point: your program gets the Promise right away. The value it stands for may show up later.

A Real-Life Promise Analogy

Booking a taxi through an app works the same way. The confirmation you get is a stand-in for a future ride, and you can keep getting ready while the driver heads toward you.

The booking eventually succeeds when the driver arrives, or fails when none is available. You don't keep checking the app for an answer; it notifies you once the booking settles.

JavaScript does the same thing: you attach handlers to a Promise, and it runs them once the asynchronous operation succeeds or fails.

How to Recognize Promises in Real Code

Promise-based code usually gives itself away through methods like .then(), .catch(), or .finally(). You might also spot new Promise(), Promise.all(), Promise.race(), or functions declared with async, which always return Promises.

Common production examples include:

  • fetch() requests
  • database queries
  • file-system operations
  • authentication workflows
  • dynamic module imports
  • timers wrapped in Promise-based utilities
  • parallel API requests
  • async and await expressions

A function tends to return a Promise when it can't produce its result on the spot. Variable names like requestPromise are a hint, but the clearest signal is the documentation or a return type such as Promise<User> in TypeScript.

What Is a JavaScript Promise?

A JavaScript Promise is an object that represents the eventual result of an asynchronous operation. It starts out pending, then becomes fulfilled with a value or rejected with a reason. You attach then, catch, and finally handlers to it to compose asynchronous steps and deal with failures, all without the nested callbacks of old.

Here's a simple example:

const deliveryPromise = new Promise((resolve) => {
  setTimeout(() => {
    resolve("Package delivered");
  }, 1000);
});

deliveryPromise.then((message) => {
  console.log(message);
  // Expected output after 1 second: Package delivered
});

The executor function you pass to new Promise() runs immediately. Calling resolve() settles the Promise successfully, yet the attached .then() handler still runs asynchronously, after the current code finishes.

How Do JavaScript Promises Work?

A Promise starts pending and settles exactly once. Calling resolve() fulfills it with a value; calling reject() rejects it with a reason. Any handlers you registered are scheduled to run later, and every call to .then(), .catch(), or .finally() returns a brand-new Promise.

1

Create the Promise

JavaScript creates a pending Promise and immediately executes the function passed to its constructor.

2

Start the operation

The executor can start a timer, network request, or another task that will finish later.

3

Settle the Promise

Calling resolve(value) fulfills the Promise. Calling reject(reason) rejects it. Later attempts to settle it are ignored.

4

Schedule the handlers

JavaScript places matching Promise reactions, such as a then or catch callback, in the microtask queue.

5

Create the next result

Each handler produces a new Promise, allowing additional asynchronous steps to be chained.

The three Promise states are:

StateMeaningCan it change?
PendingThe operation has not settledYes
FulfilledThe operation succeeded with a valueNo
RejectedThe operation failed with a reasonNo

A fulfilled or rejected Promise is called settled.

Think Like the JavaScript Engine

Behind the scenes, the runtime juggles synchronous code, external operations, and Promise microtasks, all without blocking the main call stack.

  • An execution context is created.
    When JavaScript reaches new Promise(), it creates the Promise object in memory and invokes the executor synchronously.

  • The executor enters the call stack.
    Variables declared inside it belong to its lexical environment. Nested callbacks can access surrounding variables through the scope chain.

  • External work is delegated.
    A browser may handle a timer through Web APIs or perform a network request outside the JavaScript call stack.

  • The Promise is settled.
    When the operation completes, resolve() or reject() records the result. The Promise cannot move to another state afterward.

  • Handlers enter the microtask queue.
    Promise reactions are microtasks. They run after the current call stack becomes empty and before the runtime processes the next regular task.

  • A handler produces another Promise.
    A returned value fulfills the next Promise. A thrown error rejects it. Returning another Promise makes the chain wait for that result.

  • Unused data may be collected.
    Once a Promise, its handlers, and referenced variables are unreachable, garbage collection can reclaim their memory.

This ordering is why Promise handlers always run after the synchronous statements around them:

console.log("First");

Promise.resolve().then(() => {
  console.log("Third");
});

console.log("Second");

// Expected output:
// First
// Second
// Third

Promise Chaining, Values, and Errors

Chaining connects asynchronous steps by returning a new Promise from every handler. Return a plain value and the next Promise fulfills with it. Return a Promise and the chain waits for it. Throw an error and the next Promise rejects.

Promise.resolve(5)
  .then((number) => number * 2)
  .then((number) => {
    console.log(number);
    // Expected output: 10
    return number + 1;
  })
  .then((number) => {
    console.log(number);
    // Expected output: 11
  });

Rejections move through the chain until a rejection handler processes them:

Promise.reject(new Error("Payment failed"))
  .then(() => {
    console.log("This does not run");
  })
  .catch((error) => {
    console.error(error.message);
    // Expected output: Payment failed
  })
  .finally(() => {
    console.log("Payment attempt finished");
    // Expected output: Payment attempt finished
  });

finally() is handy for cleanup, like hiding a loading spinner whether the operation succeeded or failed. Its callback doesn't receive the fulfillment value or the rejection reason, since it runs either way.

Practical JavaScript Promise Examples

Promises earn their keep when they represent real asynchronous work, not values you already have on hand synchronously.

Fetching and Validating API Data

The fetch() function returns a Promise. It rejects on network failures, but an HTTP error like 404 still counts as a "successful" response as far as fetch() is concerned, so you have to check for it yourself.

function fetchUser(userId) {
  return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`Request failed with status ${response.status}`);
      }

      return response.json();
    });
}

fetchUser(1)
  .then((user) => {
    console.log(user.name);
  })
  .catch((error) => {
    console.error("Could not load user:", error.message);
  });

Running Independent Operations Together

Promise.all() waits for every input Promise and hands back the results in the same order you passed them in, regardless of which finished first. If any single input rejects, the whole thing rejects immediately.

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

const profilePromise = delay(500, { name: "Mina" });
const settingsPromise = delay(800, { theme: "dark" });

Promise.all([profilePromise, settingsPromise]).then(
  ([profile, settings]) => {
    console.log(profile.name, settings.theme);
    // Expected output after about 800ms: Mina dark
  }
);

Choosing the Right Promise Combinator

MethodSettles whenTypical use
Promise.all()All fulfill, or one rejectsRequire every result
Promise.allSettled()Every input settlesCollect successes and failures
Promise.race()First input settlesUse the earliest outcome
Promise.any()First input fulfillsUse the first successful result

Common Mistakes in JavaScript Promises

Most Promise bugs trace back to the same few habits: forgetting to return a Promise, misreading how errors propagate, or kicking off asynchronous work that nothing ever waits for.

Forgetting to Return from a Promise Chain

getUser()
  .then((user) => {
    fetchOrders(user.id); // Not returned
  })
  .then((orders) => {
    console.log(orders); // undefined
  });

Using forEach() with Asynchronous Callbacks

forEach() does not wait for Promises returned by its callback.

// โŒ Wrong: completion is not awaited
items.forEach(async (item) => {
  await saveItem(item);
});

// โœ… Correct: wait for all save operations
await Promise.all(items.map((item) => saveItem(item)));

Wrapping an Existing Promise Unnecessarily

// โŒ Wrong: redundant Promise construction
function loadUser() {
  return new Promise((resolve, reject) => {
    fetchUser(1).then(resolve).catch(reject);
  });
}

// โœ… Correct: return the existing Promise
function loadUser() {
  return fetchUser(1);
}

Catching an Error Without Recovering or Rethrowing

A .catch() handler that returns normally converts the rejection into a fulfilled Promise.

fetchUser(1)
  .catch((error) => {
    console.error(error);
    throw error; // Keep the chain rejected
  });

JavaScript Promise Best Practices

Good Promise code leaves no doubt about who owns the result, what happens on failure, and in what order things run.

Reach for new Promise() only when you're wrapping a callback-based API or need direct control over when something settles. Most Promise-based APIs can simply be returned as they are.

Promises vs Callbacks vs Async/Await

Promises and async/await give you standard, built-in composition and error propagation. Callbacks leave those details up to whatever function you handed the callback to, which is why they're harder to compose.

ApproachReadabilityError handlingComposition
CallbacksCan become nestedOften manualDifficult for multiple operations
PromisesChain-based.catch()Strong combinator support
async/awaitReads sequentiallytry...catchBuilt on Promises

async and await don't replace Promises. They're just cleaner syntax for consuming and returning them.

Real-World Uses of JavaScript Promises

Anytime an application has to react to a result that may arrive later, Promises are usually the tool doing the work.

Common production uses include:

  • loading API data for dashboards
  • submitting forms and processing payments
  • checking authentication tokens
  • uploading files
  • reading files in Node.js
  • querying databases
  • lazy-loading application modules
  • coordinating several service requests
  • adding retry and timeout behavior

Promises are also central to React data loading, Next.js server operations, test frameworks, and modern JavaScript SDKs.

JavaScript Promise Interview Questions

These come up often in interviews because they test both the syntax and a real grasp of how asynchronous execution works.

Frequently Asked Questions About Promises

Short answers to the questions developers most often run into while learning Promises.

Keep going with the concepts that build directly on what you just learned about Promises.

Async and Await in JavaScript

Learn how async functions and await provide cleaner syntax for working with Promises.

JavaScript Event Loop

Understand call-stack, microtask, and task scheduling.

Callback Functions

Compare traditional callback patterns with Promise-based workflows.

Take the JavaScript Promises Quiz

Test your knowledge of chaining, error handling, states, and microtasks.

๐Ÿ”‘ Key Takeaways

JavaScript Promises give you one standard way to represent, combine, and handle asynchronous results.

Test Your JavaScript Promise Knowledge

Can you predict Promise output order, repair a broken chain, and choose the correct combinator?

๐Ÿ‘‰ Test your knowledge with our JavaScript Promises Quiz