Using Promises

This translation is incomplete. Please help translate this article from English

Promise е обект, представляващ обработката на евентуално завършване или отказ на асинхронна операция. Тъй като повечето хора са потребители на вече създадени promises, това ръководство ще обясни използването на върнати promises, преди да разясним на тяхното създаване.

По същество promise е върнат обект, към който прикрепяте callbacks, вместо да ги предавате, като аргумент на функция.

Представете си функция createAudioFileAsync(), която асинхронно съдава аудио файлове, като на функцията биват предавани аргументи - конфигурация и две callback функции, едната, от които ще бъде извикана при успешно създаване на аудио файла, а другата при възникване на грешка.

Примерен код използващ createAudioFileAsync():

function successCallback(result) {
  console.log("Audio file ready at URL: " + result);

function failureCallback(error) {
  console.error("Error generating audio file: " + error);

createAudioFileAsync(audioSettings, successCallback, failureCallback);

…модерните функции връщат promise, към който може да подадете callbacks.

Ако createAudioFileAsync() беше рефакторирана да връща promise, използването и би било опростено, както в следния пример:

createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

Това е кратък запис на:

const promise = createAudioFileAsync(audioSettings); 
promise.then(successCallback, failureCallback);

Наричаме това, асинхронно извикване на функция. Тази конвенция има няколко преимущества. Ще разгледаме всяко едно от тях.


За разлика от "стария стил" за предаване на callbacks, promise идва с някои гаранции:

  • Callbacks никога няма да бъдат извикани преди завършването на текущото изпълнение на JavaScript event loop.
  • Callbacks добавени с then(), дори след успех или неуспех на асинхронна операция, ще бъдат извикани.
  • Многобройни callbacks могат да бъдат добавени, чрез използване на then() няколко пъти. Всеки един callback бива изпълнен последователно в реда, в който е бил подаден.

Едно от страхотните неща, при използването на promises е chaining.


Понякога е нужно да се изпълняват две или повече асинхронни операции последователно, където всяка следваща операция започва с резултата от предишната успешно изпълнена. Това може да бъде постигнато, чрез създаване на promise chain.

Тук е магията: then() функцията връща нов promise, различен от оригиналния:

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);


const promise2 = doSomething().then(successCallback, failureCallback);

This second promise (promise2) represents the completion not just of doSomething(), but also of the successCallback or failureCallback you passed in, which can be other asynchronous functions returning a promise. When that's the case, any callbacks added to promise2 get queued behind the promise returned by either successCallback or failureCallback.

Basically, each promise represents the completion of another asynchronous step in the chain.

In the old days, doing several asynchronous operations in a row would lead to the classic callback pyramid of doom:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

With modern functions, we attach our callbacks to the returned promises instead, forming a promise chain:

.then(function(result) {
  return doSomethingElse(result);
.then(function(newResult) {
  return doThirdThing(newResult);
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);

The arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback). You might see this expressed with arrow functions instead:

.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);

Important: Always return results, otherwise callbacks won't catch the result of a previous promise (with arrow functions () => x is short for () => { return x; }).

Chaining after a catch

It's possible to chain after a failure, i.e. a catch, which is useful to accomplish new actions even after an action failed in the chain. Read the following example:

new Promise((resolve, reject) => {

.then(() => {
    throw new Error('Something failed');
    console.log('Do this');
.catch(() => {
    console.error('Do that');
.then(() => {
    console.log('Do this, no matter what happened before');

This will output the following text:

Do that
Do this, no matter what happened before

Note: The text Do this is not displayed because the Something failed error caused a rejection.

Error propagation

You might recall seeing failureCallback three times in the pyramid of doom earlier, compared to only once at the end of the promise chain:

.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))

If there's an exception, the browser will look down the chain for .catch() handlers or onRejected. This is very much modeled after how synchronous code works:

try {
  const result = syncDoSomething();
  const newResult = syncDoSomethingElse(result);
  const finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {

This symmetry with asynchronous code culminates in the async/await syntactic sugar in ECMAScript 2017:

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {

It builds on promises, e.g. doSomething() is the same function as before. You can read more about the syntax here.

Promises solve a fundamental flaw with the callback pyramid of doom, by catching all errors, even thrown exceptions and programming errors. This is essential for functional composition of asynchronous operations.

Promise rejection events

Whenever a promise is rejected, one of two events is sent to the global scope (generally, this is either the window or, if being used in a web worker, it's the Worker or other worker-based interface). The two events are:

Sent when a promise is rejected, after that rejection has been handled by the executor's reject function.
Sent when a promise is rejected but there is no rejection handler available.

In both cases, the event (of type PromiseRejectionEvent) has as members a promise property indicating the promise that was rejected, and a reason property that provides the reason given for the promise to be rejected.

These make it possible to offer fallback error handling for promises, as well as to help debug issues with your promise management. These handlers are global per context, so all errors will go to the same event handlers, regardless of source.

One case of special usefulness: when writing code for Node.js, it's common that modules you include in your project may have unhandled rejected promises. These get logged to the console by the Node runtime. You can capture these for analysis and handling by your code—or just to avoid having them cluttering up your output—by adding a handler for the unhandledrejection event, like this:

window.addEventListener("unhandledrejection", event => {
  /* You might start here by adding code to examine the
     promise specified by event.promise and the reason in
     event.reason */

}, false);

By calling the event's preventDefault() method, you tell the JavaScript runtime not to do its default action when rejected promises go unhandled. That default action usually involves logging the error to console, and this is indeed the case for Node.

Ideally, of course, you should examine the rejected promises to make sure none of them are actual code bugs before just discarding these events.

Creating a Promise around an old callback API

A Promise can be created from scratch using its constructor. This should be needed only to wrap old APIs.

In an ideal world, all asynchronous functions would already return promises. Unfortunately, some APIs still expect success and/or failure callbacks to be passed in the old way. The most obvious example is the setTimeout() function:

setTimeout(() => saySomething("10 seconds passed"), 10*1000);

Mixing old-style callbacks and promises is problematic. If saySomething() fails or contains a programming error, nothing catches it. setTimeout is to blame for this.

Luckily we can wrap setTimeout in a promise. Best practice is to wrap problematic functions at the lowest possible level, and then never call them directly again:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10*1000).then(() => saySomething("10 seconds")).catch(failureCallback);

Basically, the promise constructor takes an executor function that lets us resolve or reject a promise manually. Since setTimeout() doesn't really fail, we left out reject in this case.


Promise.resolve() and Promise.reject() are shortcuts to manually create an already resolved or rejected promise respectively. This can be useful at times.

Promise.all() and Promise.race() are two composition tools for running asynchronous operations in parallel.

We can start operations in parallel and wait for them all to finish like this:

Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });

Sequential composition is possible using some clever JavaScript:

[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });

Basically, we reduce an array of asynchronous functions down to a promise chain equivalent to: Promise.resolve().then(func1).then(func2).then(func3);

This can be made into a reusable compose function, which is common in functional programming:

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

The composeAsync() function will accept any number of functions as arguments, and will return a new function that accepts an initial value to be passed through the composition pipeline:

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

In ECMAScript 2017, sequential composition can be done more simply with async/await:

let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
/* use last result (i.e. result3) */


To avoid surprises, functions passed to then() will never be called synchronously, even with an already-resolved promise:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later when the queue is emptied at the end of the current run of the JavaScript event loop, i.e. pretty soon:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4


Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition. See common mistakes.

Nesting is a control structure to limit the scope of catch statements. Specifically, a nested catch only catches failures in its scope and below, not errors higher up in the chain outside the nested scope. When used correctly, this gives greater precision in error recovery:

.then(result => doSomethingOptional(result)
  .then(optionalResult => doSomethingExtraNice(optionalResult))
  .catch(e => {})) // Ignore if optional stuff fails; proceed.
.then(() => moreCriticalStuff())
.catch(e => console.error("Critical failure: " + e.message));

Note that the optional steps here are nested, not from the indentation, but from the precarious placement of the outer ( and ) around them.

The inner neutralizing catch statement only catches failures from doSomethingOptional() and doSomethingExtraNice(), after which the code resumes with moreCriticalStuff(). Importantly, if doSomethingCritical() fails, its error is caught by the final (outer) catch only.

Common mistakes

Here are some common mistakes to watch out for when composing promise chains. Several of these mistakes manifest in the following example:

// Bad example! Spot 3 mistakes!

doSomething().then(function(result) {
  doSomethingElse(result) // Forgot to return promise from inner chain + unnecessary nesting
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Forgot to terminate chain with a catch!

The first mistake is to not chain things together properly. This happens when we create a new promise but forget to return it. As a consequence, the chain is broken, or rather, we have two independent chains racing. This means doFourthThing() won't wait for   doSomethingElse() or doThirdThing() to finish, and will run in parallel with them, likely unintended. Separate chains also have separate error handling, leading to uncaught errors.

The second mistake is to nest unnecessarily, enabling the first mistake. Nesting also limits the scope of inner error handlers, which—if unintended—can lead to uncaught errors. A variant of this is the promise constructor anti-pattern, which combines nesting with redundant use of the promise constructor to wrap code that already uses promises.

The third mistake is forgetting to terminate chains with catch. Unterminated promise chains lead to uncaught promise rejections in most browsers.

A good rule-of-thumb is to always either return or terminate promise chains, and as soon as you get a new promise, return it immediately, to flatten things:

.then(function(result) {
  return doSomethingElse(result);
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.error(error));

Note that () => x is short for () => { return x; }.

Now we have a single deterministic chain with proper error handling.

Using async/await addresses most, if not all of these problems—the tradeoff being that the most common mistake with that syntax is forgetting the await keyword.

When promises and tasks collide

If you run into situations in which you have promises and tasks (such as events or callbacks) which are firing in unpredictable orders, it's possible you may benefit from using a microtask to check status or balance out your promises when promises are created conditionally.

One situation in which microtasks can be used to ensure that the ordering of execution is always consistent is when promises are used in one clause of an if...else statement (or other conditional statement), but not in the other clause. Consider code such as this:

customElement.prototype.getData = url => {
  if (this.cache[url]) { = this.cache[url];
    this.dispatchEvent(new Event("load"));
  } else {
    fetch(url).then(result => result.arrayBuffer()).then(data => {
      this.cache[url] = data; = data;
      this.dispatchEvent(new Event("load"));

The problem introduced here is that by using a task in one branch of the if...else statement (in the case in which the image is available in the cache) but having promises involved in the else clause, we have a situation in which the order of operations can vary; for example, as seen below.

element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data...");
console.log("Data fetched");

Executing this code twice in a row gives the results shown in the table below:

Results when data isn't cached (left) vs. when it is found in the cache
Data isn't cached Data is cached
Fetching data
Data fetched
Loaded data
Fetching data
Loaded data
Data fetched

Even worse, sometimes the element's data property will be set and other times it won't be by the time this code finishes running.

We can ensure consistent ordering of these operations by using a microtask in the if clause to balance the two clauses:

customElement.prototype.getData = url => {
  if (this.cache[url]) {
    queueMicrotask(() => { = this.cache[url];
      this.dispatchEvent(new Event("load"));
  } else {
    fetch(url).then(result => result.arrayBuffer()).then(data => {
      this.cache[url] = data; = data;
      this.dispatchEvent(new Event("load"));

This balances the clauses by having both situations handle the setting of data and firing of the load event within a microtask (using queueMicrotask() in the if clause and using the promises used by fetch() in the else clause).

If you think microtasks may help solve this problem, see the microtask guide to learn more about how to use queueMicrotask() to enqueue a function as a microtask.

See also