Promise

In JavaScript, a promise is a unique object linking the “producing code” and the “consuming code”. In other words, it is a “subscription list”.

The “producing code” takes the time it needs to produce the promised result, and the promise makes the result available to the overall subscribed code whenever it’s ready.

But, JavaScript promises are more complicated that the described simple subscription list: there are additional features and limitations inside them.

Let’s start at the very beginning.

The constructor syntax for a promise object looks like this:

// load and execute the script at the given path
let promise = new Promise(function (resolve, reject) {
  // executor (the producing code, "actor")
});

The function, which is passed to the new Promise, is named the executor. Whenever a new Promise is generated, the executor automatically runs. It involves the producing code that should eventually produce the result. The arguments of it, such as the resolve and reject, are considered callbacks provided by JavaScript.

When the executor has the result, it must call one of the following callbacks:

  • resolve(value): in case the work is finished with result value.
  • reject(error): in case there was an error, the error is the error object.

The object promise returned by the new Promise constructor contains the following internal properties:

  • state: initially "pending", then it changes either to "fulfilled" when you call resolve or "rejected" when you call reject.
  • result: it is initially undefined, then transforms into value when you call resolve(value) or resolve(value)when you call reject(error).

The executor moves the promise to one of the following states:

Now, let’s consider the example of a promise constructor along with a simple executor function with “producing code” taking time, using setTimeout:

let promise = new Promise(function (resolve, reject) {
  // the function is executed automatically when creating the promise

  // after 1.5 second signal that the job is done with the result "done"
  setTimeout(() => resolve("done"), 1500);
});

Two things can be seen by running the code mentioned above:

  1. The new Promise calls the executor at once and automatically.
  2. The executor gets two arguments: resolve and reject. The JavaScript engine pre-defines these functions. Hence you don’t have to create them. You should only call them when ready.

After “processing” one second, the executor calls resolve("done") for creating the result. It changes the condition of the promise object, like this:

So, this was an example of successful job completion.

Let’s check out an example containing an error:

let promise = new Promise(function (resolve, reject) {
  resolve("done");
  reject(new Error("…")); // ignored
  setTimeout(() => resolve("…")); // ignored
});
let promise = new Promise(function (resolve, reject) {
  // after 1.5 seconds the signal about the job is finished with an error
  setTimeout(() => reject(new Error("Error!!")), 1500);
});

As you can see, the reject(...) call moves the promise to "rejected":

A promise, which is resolved or rejected, is known as “settled”. The executor might call only a single resolve or reject. Each state change is considered final.

Any further calls of either reject or resolve are ignored.

Take a look at the following example:

let promise = new Promise(function (resolve, reject) {
  resolve("done");
  reject(new Error("…")); // ignored
  setTimeout(() => resolve("…")); // ignored
});

Also, note that reject and resolve expect a single argument, ignoring extra arguments.

Practically, an executor does something asynchronously, calling reject and resolve after some time. Also, it is possible to call resolve or reject immediately, like here:

let promise = new Promise(function (resolve, reject) {
  resolve(1234); // immediately give the result: 1234
});

Consumers: then, catch, finally

A promise object is a link between the executor and the consuming functions, that can receive a result or an error. You can register the consuming functions by using methods, such as then, .catch and .finally.

Let’s examine them one by one.

then

.then is considered the most critical, fundamental method.

Its syntax looks like this:

promise.then(
  function (result) { /* a successful result */ },
  function (error) { /* an error */ }
);

The number one argument of .then is a function, which runs whenever the promise is resolved, then receives the result.

The second argument is a function, which runs when the promise is rejected, receiving the error.

Here is an example of a successfully resolved promise reaction:

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => resolve("done!"), 1500);
});
// resolve runs the first function in .then
promise.then(
  result => console.log(result), // shows "done!" after 1.5 second
  error => console.log(error) // doesn't run
);

The following example visualizes the case of rejection:

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error("Error!!")), 1500);
});
// reject runs the second function in .then
promise.then(
  result => console.log(result), // doesn't run
  error => console.log(error) // shows "Error: Error!!" after 1.5 second
);

In case you are interested only in the successful completion, you may provide only one function argument, as follows:

let promise = new Promise(resolve => {
  setTimeout(() => resolve("done"), 1500);
});
promise.then(console.log); // shows "done" after 1.5 second

catch

In case, you are interested in errors, you should use null as the first argument, like this: .then(null, errorHandlingFunction). Either you can use .catch(errorHandlingFunction), like this:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Error!!")), 1500);
});
// .catch(f) is the same as promise.then(null, f)
promise.catch(console.log); // shows "Error: Error!!" after 1.5 second

So, the .catch(f) can be considered a shorthand of the .then(null, f).

finally

The .finally(f) call in promises is almost the same as .then(f, f) in the sense that f runs whenever the promise is settled: no matter resolved or rejected.

It’s a handy method to perform cleanup: stop the loading indicators, when they are not necessary any longer.

The example looks like this:

new Promise((resolve, reject) => {
    /* do something that takes time, and then call resolve/reject */
  })
  // runs when the promise is settled, doesn't matter successfully or not
  .finally(() => stop loading indicator)
  .then(result => show result, err => show error)

Take into account that it’s not just an alias of then(f,f). The following differences can be outlined:

  • The finally handler doesn’t have any arguments. In it we don’t know the promise is successful or not. As a rule, its task is to finalize procedures.
  • The finally handler is aimed at passing through results and errors to the next handler.

Here is an example:

new Promise((resolve, reject) => {
    setTimeout(() => resolve("result"), 2000)
  })
  .finally(() => console.log("Promise ready"))
  .then(result => console.log(result)); // .then handles the result

Also note that .finally(f) is a more convenient syntax than .then(f, f) as you don’t need to duplicate the function f

Another essential thing to know is that if a promise is pending .then/catch/finally handlers are waiting for it. But if a promise is already settled, they execute at once:

//the promise becomes resolved right after creation.
let promise = new Promise(resolve => resolve("done"));
promise.then(console.log); // done

Promises are quite flexible: you can add handlers anytime, and in case the result is there, they get it immediately.

Now, let’s get to more practical examples of how promises can help in writing asynchronous code.

Example: loadScript

As we already stated in the previous chapter, the loadScript function is used to load a script.

This is an example of its usage in the callback:

function loadScript(src, callback) {
  let createScript = document.createElement('script');
  createScript.src = src;
  createScript.onload = () => callback(null, createScript);
  createScript.onerror = () => callback(new Error(`Script load error for ${src}`));
  document.head.append(createScript);
}

If you want to rewrite it using the promises, you need to act as follows:

function loadScript(src) {
  return new Promise(function (resolve, reject) {
    let createScript = document.createElement('script');
    createScript.src = src;
    createScript.onload = () => resolve(createScript);
    createScript.onerror = () => reject(new Error(`Script load error for ${src}`));
    document.head.append(createScript);
  });
}

As you can see in the above-given example, the new function loadScript doesn’t require a callback. Yet it creates and returns a promise object, which resolves when the loading is done. The outer code might add handlers to it applying .then.

The usage is the following:

function loadScript(src) {
  return new Promise(function (resolve, reject) {
    let createScript = document.createElement('script');
    createScript.src = src;
    createScript.onload = () => resolve(createScript);
    createScript.onerror = () => reject(new Error(`Script load error for ${src}`));
    document.head.append(createScript);
  });
}

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
  script => console.log(`${script.src} is loaded`),
  error => console.log(`Error: ${error.message}`)
);
promise.then(script => console.log('Another handler'));



Do you find this helpful?

Related articles