Chapter 11: Introduction: callbacks

11.1 Introduction: callbacks

Pyramid of Doom

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

loadScript('1.js', function(error, script) {
  if (error) {
    handleError(error);
  } else {
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
...

The above example is sometimes called “callback hell” or “pyramid of doom.”

Although we can split the function into parts, it's difficult to read. Also the functions are all of single use, so there’s a bit of namespace cluttering here.

11.2

Promise

let promise = new Promise(function(resolve, reject) {
  // executor (the producing code, "singer")
});

When new Promise is created, the executor (function) runs automatically.

  • resolve(value) — called with the result value if the job finished successfully.

  • reject(error) — called if an error occurred, error is the error object.

The promise object has these internal properties:

  • state: pending -> fulfilled (when resolve is called) / rejected (when reject is called)

  • result: undefined -> the value or the error

For example:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done"), 1000);
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});
  1. The executor is called automatically and immediately.

  2. The executor receives two arguments: resolve and reject from the JavaScript engine.

  3. After one second of “processing” the executor calls resolve("done") to produce the result.

There can be only a single result or an error. Further calls are ignored.

Consumers: then, catch, finally

then

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

The second argument is optional.

catch

If we are only interested in errors:

  • .then(null, errorHandlingFunction)

  • .catch(errorHandlingFunction)

finally

finally always runs when the promise is either resolve or reject. It's a good handler to perform cleanup.

  • A finally handler has no arguments, thus we don’t know whether the promise is successful or not.

  • A finally handler passes through results and errors to the next handler.

11.2 Promises chaining

When a handler returns a value, it becomes the result of that promise, so the next .then is called with it.

Technically we can also add many .then to a single promise. This is not chaining.

Returning promises

A handler, used in .then(handler) may create and return a promise.

new Promise(function(resolve, reject) {
    ...
}).then(function(result) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(
    ...

To be precise, a handler may return an arbitrary object that has a method .then. The idea is that 3rd-party libraries may implement “promise-compatible” objects of their own.

As a good practice, an asynchronous action should always return a promise to keep it chainable.

11.4 Error handling with promises

When a promise rejects, the control jumps to the closest rejection handler. catch may appear after one or maybe several .then.

Implicit try…catch

The code of a promise executor and promise handlers has an "invisible try..catch" around it. It automatically catches the error and turns it into rejected promise.

Unhandled rejections

Unhandled promise rejections will cause the script dies with a message in the console.

In the browser we can catch such errors using the event unhandledrejection.

11.5 Promise API

Promise.all

Promise.all takes an of promises and values and returns a new promise. The new promise returns the array of the results in the same order as the iterable.

If any of the promises is rejected, the promise returned by Promise.all immediately rejects with that error, and the results of other promises are ignored but not canceled.

Promise.allSettled

Promise.allSettled just waits for all promises to settle, regardless of the result.

  • {status:"fulfilled", value:result}

  • {status:"rejected", reason:error}

Promise.race

Promise.race waits only for the first settled promise and gets its result (or error).

let promise = Promise.race(iterable);

Promise.resolve/Promise.reject

Promise.resolve(value) creates a resolved promise with the result value. Promise.reject(error) creates a rejected promise with error.

11.6 Promisification

Promisification is the conversion of a function that accepts a callback into a function that returns a promise.

function promisify(f) {
  return function (...args) { // return a wrapper-function
    return new Promise((resolve, reject) => {
      function callback(err, result) { // our custom callback for f
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // append our custom callback to the end of f arguments

      f.call(this, ...args); // call the original function
    });
  };
};

The limitation of promise is that it may have only one result, but a callback may technically be called many times.

11.7 Microtasks

let promise = Promise.resolve();

promise.then(() => alert("promise done!"));

alert("code finished"); // this alert shows first

Microtasks queue

  • The queue is first-in-first-out: tasks enqueued first are run first.

  • Execution of a task is initiated only when nothing else is running.

When a promise is ready, its .then/catch/finally handlers are put into the queue. When the JavaScript engine becomes free from the current code, it takes a task from the queue and executes it.

Unhandled rejection

An “unhandled rejection” occurs when a promise error is not handled at the end of the microtask queue.

11.8 Async/await

Async

The word “async” before a function means that the function always returns a promise.

This function returns a resolved promise with the result of 1:

async function f() {
  return 1;
}

Await

let value = await promise;

The keyword await makes JavaScript wait until that promise settles and returns its result.

  • await literally suspends the function execution until the promise settles, and then resumes it with the promise result.

  • await won’t work in the top-level code.

  • await accepts thenables to support third-party object.

Error handling

In the case of a rejection, await promise throws the error, just as if there were a throw statement at that line.

We can catch that error using try..catch, the same way as a regular throw.

Last updated