Why Promises?

Picture yourself planning a large event with multiple dependencies. You need a caterer to confirm the menu, a florist to handle the decorations, and a DJ for music. Each vendor operates on their own timeline, so you don’t want to wait on one before contacting the next—you want them to proceed in parallel or in a structured order without creating a tangled mess of back-and-forth calls. JavaScript promises offer a similar convenience for handling asynchronous operations, preventing you from getting stuck in callback hell.

In this lesson, you'll discover:


The Problem: Callback Hell

Let’s say you want to:

  1. 1. Get the user's geolocation
  2. 2. Fetch the user’s nearest surf spot from an API
  3. 3. Fetch surf conditions from yet another API

If you rely purely on callbacks, you may wind up nesting them like Russian dolls:

function getForecastForLocation() {
  locationRequest({
    success: spotRequest({
      success: forecastRequest({
        success: handleSuccess,
        error: handleError
      }),
      error: handleError
    }),
    error: handleError
  });
}

This can lead to deeply nested code that’s tough to read, debug, and maintain. That dreaded “callback hell” is like having multiple event planners constantly trying to schedule each other in the correct order, each with their own error handling.


The Solution: Promises

Promises provide a cleaner, more linear way to handle asynchronous flows. They let you chain successive actions instead of nesting callbacks deeply. Here’s how you might transform the above code using promises:

function getForecastForLocation() {
  locationRequest()
    .then(spotRequest)
    .then(forecastRequest)
    .then(handleSuccess)
    .catch(handleError);
}

With promises:

Think of it like a well-organized line of tasks: each step hands off its completed work to the next step, and if something fails, everything diverts to a single error management plan.


Terminology and Core Promise Concepts

It helps to understand a few terms:

A promise can’t be fulfilled and then become rejected later, or vice versa. Once it’s settled, that’s the final outcome. If callbacks are added after it has already settled, those callbacks will immediately execute (assuming they match the state).


Creating a Promise

You can create a new promise using the Promise constructor function. It accepts an executor function that has two parameters: resolve and reject.

const p = new Promise((resolve, reject) => {
  // If everything goes well
  if (/* success condition */) {
    resolve(/* some data */);
  } else {
    reject(/* some error */);
  }
});

resolve is like telling the promise, “All good—here’s the result.”
reject is like saying, “We had a problem—here’s the error.”

const request = new Promise(resolve => {
  setTimeout(() => resolve('Response received!'), 1000);
});

request.then(msg => console.log(msg));
// After one second -> logs: "Response received!"

In this snippet, then is invoked once setTimeout completes after one second, passing "Response received!" into the callback.


then and catch

A promise object has two critical methods:

then returns another promise, which is why you can chain them together. It’s like each successful step queues up for the next.

p.then(onFulfilled)
 .then(onFulfilledNext)
 .catch(onRejected);

catch is essentially shorthand for then(null, onRejected), letting you handle errors in one place rather than sprinkling them everywhere.

Example:

p.then(result => {
  console.log('Got result:', result);
  return doAnotherAsyncTask();
}).catch(error => {
  console.error('Error happened:', error);
});

If p fails, the catch callback triggers; if doAnotherAsyncTask() fails, the error also flows to the same catch.


Using Promises with AJAX (jQuery Example)

Many libraries and frameworks have features that return promises. For example, jQuery’s $.ajax method can be used like a promise (it actually returns a jqXHR object, but it behaves similarly).

const fetchCat = catId => (
  $.ajax({ url: `/cats/${catId}` })
);

fetchCat(1)
  .then(cat => console.log('Fetched cat:', cat))
  .fail(err => console.error('Failed to fetch cat:', err));

Notice that fail is used instead of catch in jQuery. Different promise-like implementations may have slightly differing method names, but the concept remains the same—one callback for success, one for error.


Promises Excel at Error Handling & Separation of Concerns

Instead of forcing every function to deal with possible failures, you can rely on chained .then handlers, culminating in a single .catch for errors. This keeps your code more modular and makes debugging easier.

A function that fetches data doesn't have to know what to do if the data is missing or the network fails— it merely passes any issues on to the promise chain’s error handler.


Advanced Topics


Summary

Promises are a key piece of modern JavaScript, making asynchronous code more manageable and less error-prone. They allow you to handle success and failure cleanly, avoiding “callback pyramids of doom.” By chaining then and catch, you can neatly separate your concerns, orchestrate multiple async tasks, and handle errors centrally.

Remember:

As you build complex applications with real-time data fetching, user interactions, and asynchronous workflows, mastering promises will save you countless headaches and produce cleaner, more robust code.