Why Promises Matter

JavaScript is single-threaded, which means two bits of script can't run simultaneously. In the browser, JavaScript typically shares a thread with painting, style updates, and user interaction handling. Any one of these activities delays the others.

Events and callbacks are the traditional workarounds for async operations, but they come with inherent limitations. Events work well for things that can happen multiple times on the same object — like keyup or touchstart — where catching every occurrence matters less than responding to current ones. For async success/failure, you want to react to an outcome regardless of when it happens. That's exactly what promises provide.

At their core, promises behave like event listeners with two critical differences:

  • A promise can only succeed or fail once. It can't switch between states after settling.
  • If a promise has already settled and you attach a callback later, the correct callback still fires — the timing of attachment doesn't matter.

Promise Terminology

A promise can be in one of these states:

  • fulfilled — the action succeeded
  • rejected — the action failed
  • pending — hasn't fulfilled or rejected yet
  • settled — has fulfilled or rejected

The spec also uses the term thenable to describe a promise-like object with a then method.

Promises in JavaScript

Promise libraries like Q, when, WinJS, and RSVP.js have existed for a while, all following the standardized Promises/A+ behavior. JavaScript's native implementation is closest in API to RSVP.js.

jQuery's Deferreds, however, aren't Promises/A+ compliant, which makes them subtly different and less useful. jQuery also ships a Promise type that's a subset of Deferred and inherits the same issues.

Creating a promise:

// BLOCK_5 placeholder — promise constructor example

The constructor takes one callback with two parameters: resolve and reject. You perform your async work inside the callback, then call resolve on success or reject on failure.

Consuming that promise:

// BLOCK_6 placeholder — then() usage example

then() takes two optional callbacks — one for success, one for failure. You can provide either independently.

Promises originally appeared in the DOM as "Futures," were renamed to "Promises," and eventually moved into JavaScript proper. This means they're available outside browser contexts like Node.js. New DOM APIs with async operations — including Quota Management, Font Load Events, ServiceWorker, and Streams — already use them.

Working With Other Libraries

The native API treats any object with a then() method as thenable, so promises from Q or similar libraries interoperate cleanly.

jQuery's Deferreds are problematic, but you can cast them to standard promises:

// BLOCK_7 placeholder — Converting jQuery Deferred to native promise

Here, Promise.resolve() converts the Deferred returned by $.ajax into a native promise. Watch out for two quirks: jQuery Deferreds may pass multiple arguments to their callbacks, while native promises only pass the first — which is usually what you want anyway — and jQuery doesn't follow the Error object convention for rejections.

Practical Example: Promisifying XMLHttpRequest

A realistic pattern is fetching multiple data sources, then acting when all complete. Here's how to build a simple GET helper:

// BLOCK_10 placeholder — XMLHttpRequest wrapper returning a promise

And its usage:

// BLOCK_11 placeholder — Using the promise-based HTTP helper

This wrapper eliminates direct XMLHttpRequest interactions, which makes async HTTP code substantially cleaner. Combine it with error handling — like stopping a loading spinner — and you get readable, maintainable flow control for complex multi-step operations.

Going Step by Step: Chaining and Sequencing

then() calls can be chained to transform values or to run asynchronous actions in a specific order. The key is what you return from the callback.

Returning a plain value from a then() callback passes that value to the next then() in the chain. This is a clean way to transform data step by step. For instance, you can reshape a fetch result without cluttering your main flow:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

Because JSON.parse() takes a single argument and returns a new value, it can be used directly as the callback to then():

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

This pattern is so convenient that you can create a dedicated getJSON() helper that encapsulates both the fetch and the parse:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

The real power of chaining comes when you return a promise from a callback. In that case, the next then() won't fire until that returned promise settles. This lets you queue up dependent asynchronous operations:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

This pattern is useful for reusing results. A getChapter() function can cache the initial fetch of story.json, ensuring the file is only downloaded once no matter how many chapters you retrieve:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

Handling Failures Gracefully

Promises offer two ways to handle rejections. The second argument to then() handles a rejection from the previous step only:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

Alternatively, catch() is a more readable alias for then(undefined, func). However, there is a critical difference between these two approaches:

With then(func1, func2), only one of the two callbacks runs. With then(func1).catch(func2), both can run in sequence. If func1 itself rejects, the catch() at the end of the chain will capture that error. This allows for a structure similar to JavaScript's try/catch:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

Promises also handle errors thrown in code. If an exception is raised inside the promise constructor callback, the promise automatically rejects. The same applies to errors thrown inside a then() callback—they turn into rejections that flow down the chain:

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Here is a practical use case. When fetching chapters, a catch() at the end of the chain will handle any failure, whether it is a network error in the fetch or a parsing error in the JSON response:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

If you only need to log an error without stopping the chain, you can rethrow it inside the catch() callback:

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

Parallelism and Order Combined

The async logic can be tricky to visualize. One approach is to write the code as if it were synchronous first, and then refactor it to use promises.

A simple loop over chapter URLs won't work with async operations. forEach is not async-aware, so chapters would appear in whatever order they happen to finish downloading.

To maintain order while fetching sequentially, build a chain of promises using then(). The Promise.resolve() method is useful here; it returns a promise that resolves with a given value (or undefined) and is a good starting point for a chain. The Promise.reject() method works in a similar way but forces a rejection.

You can also use array.reduce() to turn an array of URLs into a sequence of chained promises without needing a temporary variable:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

However, fetching chapters one after another takes longer than fetching them in parallel. To download them all at once and process the results in a specific order, use Promise.all(). It takes an array of promises and returns a single promise that fulfills when every input promise succeeds, yielding an array of results in the original input order:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

This is functionally correct, but you can improve the perceived performance further. Start by fetching all chapters simultaneously. Then, add them to the page in a chain. This way, the first chapter can be displayed to the user as soon as it arrives, before the rest have finished loading:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

There is a balance between achieving parallelism for speed and sequencing for correctness. With promises, you can have both: fetch everything at once, but present the results in the proper order.