The original:
ES6 Promises: Patterns and Anti-Patterns


By Bobby Brennan

When I first started using NodeJS a few years ago, I was baffled by what is now called “callback hell.” Fortunately, it’s 2017 and NodeJS has embraced a lot of the latest features of JavaScript, including Promise support since V4.

While Promises can make code cleaner and more readable, they might be a bit of a surprise to those who are only familiar with callback functions. Here, I’ll list some of the basic patterns I learned using Promise, as well as some of the pitfalls I stepped into.

Note: arrow functions will be used in this article. If you are not familiar with them, they are quite simple, so it is recommended to read the benefits of using them first

Patterns and best practices

The use of Promise

If you’re using a third-party library that already supports Promise, it’s pretty simple to use. There are only two functions to worry about: then() and catch(). For example, there is a client API that contains three methods, getItem(), updateItem(), and deleteItem(), each of which returns a Promise:

Promise.resolve(a)
  .then(_ = > {
    return api.getItem(1)
  })
  .then(item = > {
    item.amount++
    return api.updateItem(1. item);
  })
  .then(update = > {
    return api.deleteItem(1);
  })
  .catch(e = > {
    console.log('error while working on item 1');
  })
Copy the code

Each call to THEN () creates a new step in the Promise chain, and if an error occurs anywhere in the chain, the following catch() is triggered. Then () and catch() can both return a value or a new Promise, and the result will be passed to the next THEN () in the Promise chain.

For comparison, we use the callback function to implement the same logic:

api.getItem(1. (err. data) = > {
  if (err) throw err;
  item.amount++;
  api.updateItem(1. item. (err. update) = > {
    if (err) throw err;
    api.deleteItem(1. (err) = > {
      if (err) throw err;
    })
  })
})
Copy the code

The first difference to note is that with callbacks, we have to do error handling at every step of the procedure, rather than a single catch-all. The second problem with the callback function is more straightforward; each step is indented horizontally, whereas code that uses Promise has an obvious sequential relationship.

The callback function promisesize

The first trick to learn is how to convert callback functions into promises. You might be using a library still based on callbacks, or your own old code, but don’t worry, because it only takes a few lines of code to wrap it up as a Promise. Here’s an example of converting one of Node’s callback methods, fs.readfile, to a Promise:

function readFilePromise(filename) {
  return new Promise((resolve. reject) = > {
    fs.readFile(filename. 'utf8'. (err. data) = > {
      if (err) reject(err);
      else resolve(data);
    })
  })
}

readFilePromise('index.html')
  .then(data = > console.log(data))
  .catch(e = > console.log(e))
Copy the code

The key part is the Promise constructor, which takes as an argument a function with two function arguments: resolve and reject. All work is done in this function, and when it’s done, resolve is called on success, reject is called on error.

Note that only one resolve or reject is called, which should only be called once. In our example, if fs.readfile returns an error, we pass the error to reject, otherwise we pass the file data to resolve.

The value of the Promise

ES6 has two handy helper functions for creating promises from normal values: promise.resolve () and promise.reject (). For example, you might need a function that returns a Promise when some cases are handled synchronously:

function readFilePromise(filename) {
  if (!filename) {
    return Promise.reject(new Error("Filename not specified"));
  }
  if (filename = = = 'index.html') {
    return Promise.resolve('

Hello!

'
); } return new Promise((resolve. reject) = > {/ *... * /}) } Copy the code

Note that while you can pass anything (or no value) to promise.reject (), it’s good practice to pass an Error.

Run in parallel

Promise.all is a method that runs the Promise array in parallel, that is, simultaneously. For example, we have a list of files to read from disk. Using the readFilePromise function created above, it would look like this:

let filenames = ['index.html'. 'blog.html'. 'terms.html'];

Promise.all(filenames.map(readFilePromise))
  .then(files = > {
    console.log('index:'. files[0]);
    console.log('blog:'. files[1]);
    console.log('terms:'. files[2]);
  })
Copy the code

I wouldn’t even try to write the equivalent using traditional callback functions, which would be messy and error-prone.

Serial operation

Sometimes running a bunch of promises at the same time can be problematic. For example, if you try to retrieve a bunch of resources using the PROMISe.all API, you might start responding to 429 errors when the rate limit is reached.

One solution is to run promises serially, or one after the other. But ES6 doesn’t provide a method like Promise.all (why?). , but we can use array.reduce to achieve:

let itemIDs = [1. 2. 3. 4. 5];

itemIDs.reduce((promise. itemID) = > {
  return promise.then(_ = > api.deleteItem(itemID));
}, Promise.resolve());
Copy the code

In this case, we need to wait for each call to api.deleteItem() to complete before making the next call. This approach is much cleaner and more general than writing.then() for each itemID:

Promise.resolve(a)
  .then(_ = > api.deleteItem(1))
  .then(_ = > api.deleteItem(2))
  .then(_ = > api.deleteItem(3))
  .then(_ = > api.deleteItem(4))
  .then(_ = > api.deleteItem(5));
Copy the code

Race

Another handy function that ES6 provides is promise.race. As with promise.all, it takes an array of promises and runs them simultaneously, except that it returns once any Promise completes or fails, discarding all other results.

For example, we could create a Promise that times out after a few seconds:

function timeout(ms) {
  return new Promise((resolve. reject) = > {
    setTimeout(reject. ms);
  })
}

Promise.race([readFilePromise('index.html'), timeout(1000)])
  .then(data = > console.log(data))
  .catch(e = > console.log("Timed out after 1 second"))
Copy the code

Note that other promises will continue to run, just without seeing the results.

Capture the error

The most common way to catch errors is to add a.catch() code block, which catches all the previous.then() errors:

Promise.resolve(a)
  .then(_ = > api.getItem(1))
  .then(item = > {
    item.amount++;
    return api.updateItem(1. item);
  })
  .catch(e = > {
    console.log('failed to get or update item');
  })
Copy the code

Here, a catch() is emitted whenever a getItem or updateItem fails. But what if we want to deal with getItem’s errors separately? Simply insert a catch(), which can also return another Promise.

Promise.resolve(a)
  .then(_ = > api.getItem(1))
  .catch(e = > api.createItem(1. {amount: 0}))
  .then(item = > {
    item.amount++;
    return api.updateItem(1. item);
  })
  .catch(e = > {
    console.log('failed to update item');
  })
Copy the code

Now, if getItem() fails, we step in with the first catch and create a new record.

Throw an error

All code in the THEN () statement should be treated as all code inside the try block. Return promise.reject () and throw new Error() both cause the next catch() block to run.

This means that a run-time error can also trigger a catch(), so don’t assume the source of the error. For example, in the code below, we might want the catch() to only get errors thrown by getItem, but as shown in the example, it would also catch runtime errors in our THEN () statement.

api.getItem(1)
  .then(item = > {
    delete item.owner;
    console.log(item.owner.name);
  })
  .catch(e = > {
    console.log(e); // Cannot read property 'name' of undefined
  })
Copy the code

Dynamic chain

Sometimes we want to build the Promise chain dynamically, for example, by inserting an extra step when a particular condition is met. In the following example, we can choose to create a lock file before reading the given file:

function readFileAndMaybeLock(filename. createLockFile) {
  let promise = Promise.resolve(a);

  if (createLockFile) {
    promise = promise.then(_ = > writeFilePromise(filename + '.lock'. ' '))
  }

  return promise.then(_ = > readFilePromise(filename));
}
Copy the code

Be sure to rewrite promise = promise.then(/*… */) to update the Promise value. See the multiple calls to THEN () mentioned in the following anti-pattern.

anti-patterns

Promise is a neat abstraction, but it’s easy to fall into certain pitfalls. Here are some of the most common questions I encountered.

Back to hell again

When I first switched from callback functions to promises, I found it hard to shake off some old habits and still nest promises as if they were callback functions:

api.getItem(1)
  .then(item = > {
    item.amount++;
    api.updateItem(1. item)
      .then(update = > {
        api.deleteItem(1)
          .then(deletion = > {
            console.log('done! ');
          })
      })
  })
Copy the code

This nesting is completely unnecessary. Sometimes a layer or two of nesting can help combine related tasks, but it’s always best to rewrite the Promise vertical chain using.then().

Return loss

A common mistake I make is forgetting a return statement in a Promise chain. Can you spot the following bug?

api.getItem(1)
  .then(item = > {
    item.amount++;
    api.updateItem(1. item);
  })
  .then(update = > {
    return api.deleteItem(1);
  })
  .then(deletion = > {
    console.log('done! ');
  })
Copy the code

Because we didn’t write a return before api.updateItem() in line 4, the then() block will immediately resolove, causing api.deleteItem() to be called before api.updateItem() completes.

In my opinion, this is a big problem with ES6 promises, which tend to trigger unexpected behavior. The problem is that.then() can return either a value or a new Promise, and undefined is completely a valid return value. Personally, if I were in charge of the Promise API, I would throw a runtime error when.then() returns undefined, but for now we need to pay special attention to promises created by return.

Multiple calls to.then()

According to the specification, calling THEN () multiple times on the same Promise is perfectly valid, and the callbacks will be invoked in the order they were registered. However, I have not seen a scenario where this is required, and some unexpected behavior can occur when using return values and error handling:

let p = Promise.resolve('a');
p.then(_ = > 'b');
p.then(result = > {
  console.log(result) // 'a'
})

let q = Promise.resolve('a');
q = q.then(_ = > 'b');
q = q.then(result = > {
  console.log(result) // 'b'
})
Copy the code

In this example, because we don’t update the value of p every time we call then(), we don’t see the ‘b’ return. But q is updated every time THEN () is called, so its behavior is more predictable.

This also applies to error handling:

let p = Promise.resolve(a);
p.then(_ = > {throw new Error("whoops!")})
p.then(_ = > {
  console.log('hello! '); // 'hello! '
})

let q = Promise.resolve(a);
q = q.then(_ = > {throw new Error("whoops!")})
q = q.then(_ = > {
  console.log('hello'); // We never reach here
})
Copy the code

Here, we expect to throw an error to break the Promise chain, but the second then() will still be called because the value of p is not updated.

It’s possible to call.then() multiple times on a Promise for a number of reasons, because it allows the Promise to be assigned to several new, separate promises, but no real usage scenarios have been discovered.

Mix callbacks and promises

It’s easy to fall into the trap of using Promise based libraries while still working on callback-based projects. Always avoid using callbacks for THEN () or catch(), or Promise will swallow up any subsequent errors as part of the Promise chain. For example, the following might seem like a reasonable way to wrap a Promise with a callback function:

function getThing(callback) {
  api.getItem(1)
    .then(item = > callback(null. item))
    .catch(e = > callback(e));
}

getThing(function(err. thing) {
  if (err) throw err;
  console.log(thing);
})
Copy the code

The problem here is that if there’s an error, we get a warning about “Unhandled Promise Rejection” even if we add a catch() block. This is because callback() is called on both then() and catch(), making it part of the Promise chain.

If a callback must be used to wrap a Promise, use setTimeout (or process.nexttick in NodeJS) to break the Promise:

function getThing(callback) {
  api.getItem(1)
    .then(item = > setTimeout(_ = > callback(null. item)))
    .catch(e = > setTimeout(_ = > callback(e)));
}

getThing(function(err. thing) {
  if (err) throw err;
  console.log(thing);
})
Copy the code

Do not catch errors

Error handling in JavaScript is a little strange. While the familiar try/catch paradigm is supported, there is no way to force the caller to handle errors the Java way. However, with callback functions, it becomes common to use so-called “errbacks”, where the first argument is an error callback. This forces the caller to at least acknowledge the possibility of error. For example, the FS library:

fs.readFile('index.html'. 'utf8'. (err. data) = > {
  if (err) throw err;
  console.log(data);
})
Copy the code

With Promises, it’s easy to forget that error handling is necessary, especially for sensitive operations such as file system and database access. Currently, if you don’t catch a Reject Promise, you’ll see a very ugly warning in NodeJS:

(node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops!
(node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Copy the code

Make sure to add a catch() to the end of any Promise chains in the main event loop to avoid this.

conclusion

Hopefully, this was a useful overview of common Promise patterns and antipatterns. If you want to learn more, here are some useful resources:

  • Mozilla’s ES6 Promise document
  • Promise from Google
  • Dave Atchley’s OVERVIEW of ES6 Promise

More Promise patterns and anti-patterns

Or read content from the DataFire team