0

Why do native Promises act like a try/catch when it comes to syntax errors?

Promises clearly have value in flow control when you need to perform a number of async operations in order. But the necessity of having to implement your own inadequate error handing implementations for every promise sometimes makes them a hassle to work with.

Take the following code:

asdf

let one = new Promise((resolve, reject) => {
  asdf
}).catch(err => {
  console.log(err)
  console.log(err.stack)
})

The first syntax error produces a typical browser error and a stack trace with 18 entries. The promise'd version has 4.

So my question is why, when writing the spec and implementing it natively, did they retain the try/catch like functionality of the userspace implementation of promises so that it could be used for flow control but leave the standard error handling?

Jeremy
  • 1
  • 85
  • 340
  • 366
hatch
  • 317
  • 1
  • 13
  • 3
    Because that's not a syntax error, but a reference error (possibly) occuring in the future. – Bergi Apr 22 '16 at 00:33
  • 1
    Note that the errors are ReferenceErrors rather than SyntaxErrors. The latter cannot be caught as the engine can't understand well enough to execute the script. But, `asdf` is a valid identifier and accessing a variable alone is a valid statement. So, the parser finds no issues with it. – Jonathan Lonowski Apr 22 '16 at 00:33
  • Not sure whether this is a suitable duplicate: [Why are exceptions used for rejecting promises in JS?](http://stackoverflow.com/q/21616432/1048572) Please comment if your question is about some other point. – Bergi Apr 22 '16 at 00:36
  • In short: for simplicity and consistency, you don't want asynchronous functions to `throw` *and* return rejected promises, you only want to have to deal with one failure mode. – Bergi Apr 22 '16 at 00:38

3 Answers3

4

An actual syntax error will be thrown by the Javascript parser when the code loads and will still be a thrown exception. Basically when the parser decides it can't properly parse the code, there's nothing else to do at that point. It will give up and stop parsing the rest of the code.

It is runtime errors (that happen during code execution not during loading/parsing) that promises will handle.

By specification, .then() promise handlers are "throw safe" which means that they turn thrown exceptions into rejected promises. This is done because these handlers are always asynchronous and thrown exceptions in asynchronous callbacks are fairly useless because they can't be caught by the calling code since the calling code is no longer on the stack and the calling code can only be notified via callbacks. The exceptions end up just going into some asynchronous infrastructure where no calling code can intercept or handle them. So, they made a decision to turn exceptions into a rejection so that all the normal reject handling and error propagation that promises have can be used.

And, because exceptions can't be caught by the calling code, if promises didn't do this, then you'd have to do your own try/catch in pretty much every .then() handler to make sure you caught anything going wrong. So, it saves a lot of extra code and makes it easy to make sure all asynchronous exceptions are caught properly and the error is propagated back to the calling code.

In a nutshell, once you get used to it, it's incredibly useful and since an exception that goes nowhere is useless anyway, I'd say the designers made a very good choice by deciding to catch the exceptions and make them useful. FYI, you can always still do your own try/catch inside a .then() handler and do whatever you want yourself if you desire.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
1

Promises do not capture your typical syntax errors, which occur at parse/compile time. Your example isn't a SyntaxError (the parser doesn't understand what your code), it's a ReferenceError (the interpreter/runtime can't find the identifier you named).

Uncaught ReferenceError: asdf is not defined

If there was a syntax error in the literal code, it would not have been caught. This is what we should expect: if it can't parse the code, it can't know that you've even created a promise.

let one = new Promise((resolve, reject) => {
  foo bar baz
}).catch(err => {
  // ignore error
});
Uncaught SyntaxError: Unexpected identifier

Note that if you use eval to generate a syntax error at run time (from the perspective of the Promise-using code), it will be caught. The distinction is not between the JavaScript object types of the errors, the distinction is between compile time and run time errors.

let one = new Promise((resolve, reject) => {
  eval("foo bar baz");
}).catch(err => {
  console.log("Captured error:", err);
})
Captured error: SyntaxError: Unexpected identifier
Jeremy
  • 1
  • 85
  • 340
  • 366
0

The reason is because when you are dealing with an asynchronous process, you have nobody to catch your error. The previous callstack is gone.

To wit:

function simulateAsyncError () {
  try {
    setTimeout(function () { throw new Error("Nobody caught me"); }, 1);
    console.log("I have set the future error.");
  } catch (err) { console.log("I caught it!"); }
}

simulateAsyncError();
// "I have set the future error."
// Uncaught Error: "Nobody caught me"

Promises are about wrapping that, and dealing with that.

Norguard
  • 26,167
  • 5
  • 41
  • 49
  • I am wholly confused by the downvote, as the example perfectly answers the question posed at the bottom of the original post. – Norguard Apr 22 '16 at 01:30