Skip to content

Instantly share code, notes, and snippets.

@joeytwiddle
Last active February 17, 2023 05:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joeytwiddle/0e73697280ea9226f1c0 to your computer and use it in GitHub Desktop.
Save joeytwiddle/0e73697280ea9226f1c0 to your computer and use it in GitHub Desktop.

How to stop promises from swallowing your errors

TLDR? Follow the Golden Rule of Promises:

If you have a promise, you must either return it, await it, or catch it at the end of the chain.

In the case of a returned promise, it is then the responsibility of the caller to obey the Golden Rule.

Additional: Your response handler and your error handler should be written in such a way that they can never throw errors themselves, or you should follow them with another catch for peace of mind!

Examples (using console.error() as an error handler):

// Valid (returned)
function foo () {
  return Promise.resolve(99).then(bar);
}

// Valid (caught)
function foo () {
  Promise.resolve(99).then(bar).catch(console.error);
}

// Invalid (forgot to catch, or return)
function foo () {
  Promise.resolve(99).then(bar);
}

// Invalid (forgot the final catch; if bar unexpectedly throws an error, that error will not make it to the handler.)
function foo () {
  Promise.resolve(99).then(bar, console.error);
}

Update: The need to strictly follow the Golden Rule has been somewhat relaxed recently, because browsers now warn of unhandled rejections, and Node also plans to do something with them too soon (crash the process I think). So at least your errors won't be silently swallowed. (Until then, you can explicitly listen for unhandled rejections in Node.)

The original article follows. It applied to browsers around 2016 and to NodeJS version 4.


So, you've just discovered that promises without appropriate error handling will simply swallow any errors thrown from within your code. If you accidentally misspell a variable name or try to call or access through an undefined expression, your browser console will give you no indication whatsoever. Wow!

You may now be wondering how to use Promises correctly, so they won't make your life harder. Here is a quick summary.

With the Q library

If you are using the Q library, you can use its done() method to ensure errors are never lost.

The general rule is:

Always use done() at the end of your chain, unless you are going to return or pass the promise to some other part of the code.

This is pretty much what Q's documentation recommends.

However, I am going to recommend you instead follow the guidelines for ES2015 Promises below. Because, if followed correctly, that will produce the same results, but with the added benefit that your code will not be tied to the Q library, and will be able to work with a greater number of different promises libraries.

But anyway, for completeness, here are some examples of using or not using done().

promise.then(onFulfilled);                        // POOR: errors thrown anywhere in the chain will 
                                                  // disappear!

promise.then(onFulfilled, onRejected);            // POOR: any error thrown inside onFulfilled will
                                                  // disappear!

promise.then(onFulfilled).catch(onRejected);      // Better, but still any error inside onRejected will
                                                  // disappear!

promise.then(onFulfilled).catch(onRejected)       // GOOD: Errors in the promise chain or onFulfilled
    .done();                                      // will be sent to onRejected.  Errors in onRejected
                                                  // will be sent to the console.

promise.then(onFulfilled, onRejected).done();     // GOOD: Errors in the promise chain go to onRejected.
                                                  // Errors in onFulfilled or onRejected appear on the
                                                  // console.

promise.done(onFulfilled, onRejected);            // GOOD: Promise errors go to onRejected.  Errors in
                                                  // onFulfilled or onRejected appear on the console.

promise.done(onFulfilled);                        // GOOD: Q reports any errors to console.  We do not
                                                  // handle any errors.

promise.then(onFulfilled, onRejected)             // GREAT: Any errors in your handlers will be logged
       .catch( console.error.bind(console) );     // to the console, plus your code will be compatible
                                                  // with other Promises libraries, should you choose
                                                  // to switch later.

return promise.then(...);                         // GOOD: the caller is responsible for handling errors!

With ES2015 Promises

Mongoose 4 returns mPromises by default, which are Promises/A+ compliant, as are the standard ES6 promises now available in Node and browsers.

But Promises/A+ standard doesn't have a .done() at all!

In fact the original drafts of ES6 didn't specify that promises should have a .catch() method, but fortunately ES2015 and ES7 promises do, and many libraries support .catch(). So do use it if it is available!

For standard promises, the general recommendation is:

Regardless of whether you write a custom onRejected function or not, always put a .catch() at the end of the chain with a function that you are confident will never throw errors itself.

In the following examples, my "guaranteed" error reporter is console.error.

promise.then(onFulfilled, onRejected);            // Any error thrown inside onFulfilled or
                                                  // onRejected will disappear!

promise.then(onFulfilled).catch(onRejected);      // A little better, but still any error inside
                                                  // onRejected will disappear!

promise.then(onFulfilled)
       .catch( console.error.bind(console) );     // GOOD: Don't perform any specific error handling, but
                                                  // at least get informed if an error does occur.

promise.then(onFulfilled)                         // GOOD: You can handle the error in onRejected, but
       .catch(onRejected)                         // if you make a mistake in there, that error will
       .catch( console.error.bind(console) );     // still get reported, not swallowed.

return promise;                                   // GOOD

return promise.then(...);                         // GOOD

You can use any general purpose error reporter instead of console.error, but you will want to ensure that it will never throw an Error itself. Because if it does throw an error, you won't see that error, so your code will fail silently and you won't know why.

The double catch pattern, with a common never-breaking function in the last catch, is recommended by Soares Chen to let you know if an error occurs in your custom rejection handler.

Enforcing good practice

Soares has also made some suggestions for how to wrap new promises so that you can detect poor use of promises inside your application. This could be useful code to run during development, even if you don't run it in production

The idea of using a single function to create all promises has value. But for an existing app this can be a daunting task!

Therefore I would like to extend his idea with the suggestion that you wrap or monkey patch any promises libraries in use in your app, so you won't have to change existing code to use his createPromise function.

Perhaps something like this could work to universally wrap the Q library (NEEDS WORK):

$ npm install --save q-with-policing
$ ln -sf q-with-policing node_modules/q

And something like this to universally wrap Promise on the back or front end (NEEDS WORK):

// This is a module your app should load as early as possible

const promisePolice = require('promisePolice')

const world = typeof global === 'undefined' ? window : global

promisePolice.useTheOriginal(world.Promise)
world.Promise = promisePolice.createPromise

Using wrappers like those above, we should be able to police and detect poor use of promises, and swap out our wrappers when they are no longer needed, e.g. for production.

Further reading

  • Nolan Lawson's excellent article on Promise misuse. At the very bottom there is a link which shows how Bluebird counters some common mistakes made with promises.
  • Node can warn you when a promise is rejected and you haven't handled it. You need to turn it on manually. You probably should!
  • Q can also give you some info about unhandled errors.
  • There was a proposal that all the promise libraries could handle this the same way.
  • Instead of just logging unexpected errors, you can use this trick to throw the Error in a way that preserves the original stack trace.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment