136

Why can't I just throw an Error inside the catch callback and let the process handle the error as if it were in any other scope?

If I don't do console.log(err) nothing gets printed out and I know nothing about what happened. The process just ends...

Example:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

If callbacks get executed in the main thread, why the Error gets swallowed by a black hole?

Theveloper
  • 728
  • 3
  • 17
  • 34
demian85
  • 2,124
  • 2
  • 18
  • 19
  • 11
    It doesn't get swallowed by a black hole. It rejects the promise that `.catch(…)` returns. – Bergi Jun 08 '15 at 17:28
  • See also [Why are exceptions used for rejecting promises in JS?](http://stackoverflow.com/q/21616432/1048572) and [How do I handle exceptions globally with native promises in io.js / node.js?](http://stackoverflow.com/q/28709666/1048572) – Bergi Nov 07 '15 at 13:27
  • instead of `.catch((e) => { throw new Error() })`, write `.catch((e) => { return Promise.reject(new Error()) })` or simply `.catch((e) => Promise.reject(new Error()))` – chharvey Feb 03 '19 at 23:54
  • 1
    @chharvey all code snippets in your comment have exactly identical behavior, except that initial one is obviously most clear. – Сергей Гринько Feb 24 '20 at 20:14

6 Answers6

167

As others have explained, the "black hole" is because throwing inside a .catch continues the chain with a rejected promise, and you have no more catches, leading to an unterminated chain, which swallows errors (bad!)

Add one more catch to see what's happening:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

A catch in the middle of a chain is useful when you want the chain to proceed in spite of a failed step, but a re-throw is useful to continue failing after doing things like logging of information or cleanup steps, perhaps even altering which error is thrown.

Trick

To make the error show up as an error in the web console, as you originally intended, I use this trick:

.catch(function(err) { setTimeout(function() { throw err; }); });

Even the line numbers survive, so the link in web console takes me straight to the file and line where the (original) error happened.

Why it works

Any exception in a function called as a promise fulfillment or rejection handler gets automatically converted to a rejection of the promise you're supposed to return. The promise code that calls your function takes care of this.

A function called by setTimeout on the other hand, always runs from JavaScript stable state, i.e. it runs in a new cycle in the JavaScript event loop. Exceptions there aren't caught by anything, and make it to the web console. Since err holds all the information about the error, including the original stack, file and line number, it still gets reported correctly.

Vicky Chijwani
  • 9,594
  • 6
  • 53
  • 78
jib
  • 34,243
  • 11
  • 80
  • 138
  • 3
    Jib, that's an interesting trick, can you help me understand why that works? – Brian Keith Jun 11 '15 at 15:42
  • @BrianKeith that works because `setTimeout` is a timer. Since timers queue execution of their callbacks to a specified point in time in the future, they are not part of the immediate function call stack. For more information on the relationship between the call stack and event queue, there's some solid reading and diagrams on the Mozilla Developer Network here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop – James Dec 22 '15 at 20:58
  • By the same logic, when programming in node.js, using `process.nextTick` in place of `setTimeout` will perform the same action. Now I have a nice method which I can toss into the end of a bothersome Promise chain to kill the program if I screw up syntax somewhere. – Tustin2121 Dec 29 '15 at 08:35
  • 8
    About that trick: you are throwing because you want to log, so why not just log directly? This trick will at a 'random' time throw an uncatchable error.... But the whole idea of exceptions (and the way promises deal with them) is to make it the *caller's responsibility* to catch the error and deal with it. This code effectively makes it impossible for the caller to deal with the errors. Why not just make a function to handle it for you? `function logErrors(e){console.error(e)}` then use it like `do1().then(do2).catch(logErrors)`. Answer itself is great btw, +1 – Stijn de Witt Sep 07 '16 at 16:11
  • @StijndeWitt Sure, the trick is just an alternative to logging that shows up as a real error in all consoles. It's not an alternative to passing errors up. This is about what a caller does when they've caught an error that's been passed up to them. – jib Sep 07 '16 at 20:16
  • 3
    @jib I am writing an AWS lambda which contains many promises connected more or less like in this case. To exploit AWS alarms and notifications in case of errors I need to make the lambda crash throwing an error (I guess). Is the trick the only way to obtain this? – masciugo Dec 15 '16 at 17:21
  • 3
    @StijndeWitt In my case, I was trying to send error details to my server in `window.onerror` event handler. Only by doing the `setTimeout` trick can this be done. Otherwise `window.onerror` will never hear a thing about the errors happened in Promise. – hudidit Feb 25 '17 at 15:52
  • 1
    @hudidit Still, wheter it's `console.log` or `postErrorToServer`, you can just do what needs to be done. There is no reason whatever code is in `window.onerror` cannot be factored out into a separate function and be called from 2 places. It's probably even shorter then the `setTimeout` line. – Stijn de Witt Mar 05 '17 at 23:19
  • In Node JS you can also log the error and then use `process.exit(1)`, in case you want to avoid the timeout. – Pensierinmusica Mar 26 '17 at 21:35
  • can someone explain me why the `throw` inside async functions (eg. `setTimeout`) is uncaught while sync is caught by Promise `catch()` method? – Paweł Sep 03 '17 at 09:31
  • @Paweł [Avoid calling setTimeout() directly](https://stackoverflow.com/a/39027151/918910) and [return all the promises](https://stackoverflow.com/a/37084467/918910), and everything should be caught. – jib Sep 03 '17 at 14:41
  • it's incredible to see how skipping run loop and throwing error in the middle of global scope gains over 100 up votes. – Ben Affleck Sep 27 '17 at 13:57
  • Hopefully it's because it explains where the error normally goes. I mention the "trick" mostly because I think it helps illustrate how things work when dealing with a JavaScript event loop. I hope very few people actually end up doing that. The right thing to do 99% of the time is to NOT use the trick, but instead ensure that errors are propagated up the promise chain correctly, but [returning all promises](https://stackoverflow.com/a/37084467/918910). – jib Sep 27 '17 at 15:32
  • @StijndeWitt But then you get more explicit dependencies in your code. Instead of having one entry point via window.onerror, you have to reference this function from throughout your code. – Alex Jun 18 '18 at 15:02
  • *"But then you get more explicit dependencies in your code"*. That is a good thing. We want our dependencies to be explicit. – Stijn de Witt Jun 19 '18 at 10:52
47

Important things to understand here

  1. Both the then and catch functions return new promise objects.

  2. Either throwing or explicitly rejecting, will move the current promise to the rejected state.

  3. Since then and catch return new promise objects, they can be chained.

  4. If you throw or reject inside a promise handler (then or catch), it will be handled in the next rejection handler down the chaining path.

  5. As mentioned by jfriend00, the then and catch handlers are not executed synchronously. When a handler throws, it will come to an end immediately. So, the stack will be unwound and the exception would be lost. That is why throwing an exception rejects the current promise.


In your case, you are rejecting inside do1 by throwing an Error object. Now, the current promise will be in rejected state and the control will be transferred to the next handler, which is then in our case.

Since the then handler doesn't have a rejection handler, the do2 will not be executed at all. You can confirm this by using console.log inside it. Since the current promise doesn't have a rejection handler, it will also be rejected with the rejection value from the previous promise and the control will be transferred to the next handler which is catch.

As catch is a rejection handler, when you do console.log(err.stack); inside it, you are able to see the error stack trace. Now, you are throwing an Error object from it so the promise returned by catch will also be in rejected state.

Since you have not attached any rejection handler to the catch, you are not able to observe the rejection.


You can split the chain and understand this better, like this

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

The output you will get will be something like

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

Inside the catch handler 1, you are getting the value of promise object as rejected.

Same way, the promise returned by the catch handler 1, is also rejected with the same error with which the promise was rejected and we are observing it in the second catch handler.

thefourtheye
  • 206,604
  • 43
  • 412
  • 459
  • 4
    Might also be worth adding that `.then()` handlers are async (stack is unwound before they are executed) so exceptions inside them have to be turned into rejections, otherwise there would be no exception handlers to catch them. – jfriend00 Jun 08 '15 at 17:52
7

I tried the setTimeout() method detailed above...

.catch(function(err) { setTimeout(function() { throw err; }); });

Annoyingly, I found this to be completely untestable. Because it's throwing an asynchronous error, you can't wrap it inside a try/catch statement, because the catch will have stopped listening by the time error is thrown.

I reverted to just using a listener which worked perfectly and, because it's how JavaScript is meant to be used, was highly testable.

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});
RiggerTheGeek
  • 1,007
  • 1
  • 10
  • 29
3

According the spec (see 3.III.d):

d. If calling then throws an exception e,
  a. If resolvePromise or rejectPromise have been called, ignore it.
  b. Otherwise, reject promise with e as the reason.

That means that if you throw exception in then function, it will be caught and your promise will be rejected. catch don't make a sense here, it is just shortcut to .then(null, function() {})

I guess you want to log unhandled rejections in your code. Most promises libraries fires a unhandledRejection for it. Here is relevant gist with discussion about it.

just-boris
  • 8,201
  • 5
  • 41
  • 77
  • It's worth mentioning that the `unhandledRejection` hook is for server-side JavaScript, on client side different browsers have different solutions. WE have not standardized it yet but it's getting there slowly but surely. – Benjamin Gruenbaum Jun 08 '15 at 20:36
1

Yes promises swallow errors, and you can only catch them with .catch, as explained more in detail in other answers. If you are in Node.js and want to reproduce the normal throw behaviour, printing stack trace to console and exit process, you can do

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});
Jesús Carrera
  • 10,611
  • 4
  • 59
  • 55
  • 1
    No, that's not sufficient, as you would need to put that at the end of *every* promise chain you have. Rather [hook on the `unhandledRejection` event](http://stackoverflow.com/a/28004999/1048572) – Bergi Nov 07 '15 at 13:30
  • Yes, that's assuming that you chain your promises so the exit is the last function and it's not being catched after. The event that you mention I think it's only if using Bluebird. – Jesús Carrera Nov 07 '15 at 17:35
  • Bluebird, Q, when, native promises, … It'll likely become a standard. – Bergi Nov 07 '15 at 17:38
1

I know this is a bit late, but I came across this thread, and none of the solutions were easy to implement for me, so I came up with my own:

I added a little helper function which returns a promise, like so:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

Then, if I have a specific place in any of my promise chain where I want to throw an error (and reject the promise), I simply return from the above function with my constructed error, like so:

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

This way I am in control of throwing extra errors from inside the promise-chain. If you want to also handle 'normal' promise errors, you would expand your catch to treat the 'self-thrown' errors separately.

Hope this helps, it is my first stackoverflow answer!

nuudles
  • 11
  • 1
  • `Promise.reject(error)` instead of `new Promise(function (resolve, reject){ reject(error) })` (which would need a return statement anyway) – Funkodebat Jul 31 '19 at 23:12