Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Background information #1

Open
domenic opened this issue Dec 11, 2012 · 11 comments
Open

Background information #1

domenic opened this issue Dec 11, 2012 · 11 comments

Comments

@domenic
Copy link
Member

domenic commented Dec 11, 2012

This is rough and not spec-worthy, but is meant to give us a common starting point for future issues.

Terminology

  • "Unhandled rejection": the occurrence wherein a promise transitions to the rejected state, but no rejection handlers are registered for it.
  • "Handled later": if a promise is in the rejection state with no rejection handlers, but then one is added to the promise (via a then or catch call), we say that the rejection has now been "handled later." Notably, this could happen several seconds later.
  • "Crash": the behavior of a program when a synchronous exception is not handled. In Node.js and Windows 8 apps this is an actual crash; in browsers, this is an error to the console. "Crash" is just a shorthand.

Statement of the issue

  • With sync exceptions, if they get to the top of the stack with no catch blocks, then you can be sure that nobody will ever handle them, and the runtime knows your program should crash.
  • With async rejections, if they are unhandled, crashing is not the correct behavior, because they could be handled later.
  • The reason for this is that promises are first-class objects that can be passed around for handling in e.g. other program contexts or in response to other asynchronous events. But disallowing unhandled rejections (by crashing the moment they appear) essentially prohibits the use of promises in the rejected state as first-class objects.
  • But, since we can't react to unhandled rejections immediately by crashing, how should we react to them? And, if they are handled later, how should that impact the steps we took when they first appeared?

Sample Code

var promise = pendingPromise();

promise.then(function () {
    console.log("I only attached a handler for fulfillment");
});

rejectPromise(promise, new Error("who handles me?"));
// Nobody sees the error! Oh no, maybe we should crash here?

// But if we crashed there, then how would this code ever get run?
setTimeout(function () {
    promise.then(undefined, function (err) {
        console.error("I got it!", err);
    });
}, 5000);
@novemberborn
Copy link

Even though a promise is in a rejected state, it's important to know how it got to be in that state:

  • The resolver was rejected -> an intentional action
  • A handler returned a rejected promise -> potentially intentional
  • An exception was thrown by a handler

I'd argue it's only the later that should be treated as unintentional and logged/reported etc. Any errors that are part of the application contract could be communicated by returning rejected promises and handled like any regular value.

@domenic
Copy link
Member Author

domenic commented Dec 12, 2012

I'd argue it's only the later that should be treated as unintentional and logged/reported etc. Any errors that are part of the application contract could be communicated by returning rejected promises and handled like any regular value.

I strongly disagree. The entire point of exceptions (and thus their asynchronous analog, rejections) is that they're outside your control. You can't partition your failure cases into "intentional" and "unintentional."

As another counterpoint, I'd like to offer the following strawman statement:

Any return values that are part of the application contract could be communicated by returning fulfilled promises.

This seems like a large burden to place on users.

@ForbesLindesay
Copy link
Member

I definitely don't think we should differentiate based on "intentional" vs. "unintentional" errors. To some extent all errors are "intentional" - JSON.parse('<') intentionally throws a SyntaxError and web servers intentionally return 404 when you request a page that doesn't exist. At some higher level of abstraction I probably prefer to assume I'm only handling valid JSON and that all the pages I request exist. At that point the same errors become unexpected.

@briancavalier
Copy link
Member

I agree with @domenic and @ForbesLindesay: interpretation and handling of exceptions is ultimately an application developer decision, and the parallel between unhandled exceptions and rejections is, imho, way too strong to ignore. Any uncaught exception that makes its way to the host environment causes a loud stack trace, regardless of the intent with which it was originally thrown. I think we should mimic that behavior with unhandled rejections as closely as possible.

The trick is, as @domenic pointed out in Statement of the issue, that unhandled rejections have a temporal component: At what point do they become crash-worthy? Yay, it's a halting problem for promises: "Write a promise debugger that tells me if this rejected promise is ever handled".

@novemberborn
Copy link

I have a method that does a database query, and rejects the returned promise with a NotFoundError instance in case the record could not be found. It could also reject the promise with a ConnectionError instance.

My rejection handler should translate the database level NotFoundError into a MissingResource error, but not attempt to translate any other errors. The MissingResource error is expected and should not show up in any logs or be reported to an exception tracking service like Airbrake. The other errors should.

The pattern here would essentially be:

query().then(null, function(error){
  if(error && error.name === "NotFoundError"){
    return rejected(new ResourceMissingError());
  }
  throw error;
});

If the ResourceMissingError is also thrown, how would any logging code be able to tell which error is expected and which isn't?

@ForbesLindesay
Copy link
Member

It wouldn't and it shouldn't. If your application later handles that ResourceMissingError and sends a 404 (or similar) to the user it won't get tracked as an unhandled rejection. If you didn't handle it then it would've happened at an unexpected time and should be logged as unexpected.

@MicahZoltu
Copy link

I believe part of the disconnect here is what exactly is a promise? To me, a promise is a thing that will eventually end up in one of two states with one of two results. Others seem to be thinking about a promise as a thing that will eventually resolve or throw an error.

An example would be a web request. When you have a Promise<string> that is returned from something like http.getstring('http://www.google.com/'), what you have is something that will eventually be either a string or an error. I may kick off the request now but not bother to look at its result until later. I may not have enough state to deal with the result right now. This is especially true if you are prefetching/precomputing data that may be used later (or never).

It seems that most of the proposals here are operating on the assumption that one will always be handling the results of a promise immediately, not storing them off for later processing.

If promises are intended to be resolved or crash, then the above example would have to be changed to return something like a Promise<Maybe<string|Error>> where http.getstring will always resolve to a Maybe<string|Error> and only something catastrophic will result in the promise resolving to a rejected state, in which case crash the application if the user doesn't handle it. IMO, this reduces the usefulness of promises.

@MicahZoltu
Copy link

There is a lot of discussion about treating promise errors like exceptions, but promises are very different from exceptions. Exceptions unwind the call stack, promises are more akin to an if statement.

if (success)
    run sequence of success callbacks and any future attached success callbacks
if (failure)
    run sequence of failure callbacks and any future attached failure callbacks

What is being proposed is that if my code simply leaves off the if (failure) block it is somehow erroneous and I should be immediately told about it. In reality, if I leave off the if block that is either a business decision or an application authoring error. While sure, debugging tools that can let me go and find promises that were rejected may be useful in some situations, this should be a purely opt-in debugging tool just like something that lets me browse the heap.

@domenic
Copy link
Member Author

domenic commented May 26, 2015

@bergus
Copy link

bergus commented May 26, 2015

@Zoltu If you plan to cache promises, I think it is a good practise to always explicitly state how errors are handled - if you don't do it, you'll get an unhandled rejection.

For your use case, you can still do

let prefetched = fetch('http://google.com');
prefetched.then(null, function ignoreErrors(){});
// later
prefetched.then(, ); // actually deal with it

@MicahZoltu
Copy link

I believe that forcing the developer to write catches for every promise he ever interacts with is a very poor experience. Also, I may return a promise from a function that the user doesn't need to know the result to. A library may expose a method, fire(...) that returns a promise to me. If I want to fire-and-forget, an error will be logged.

It feels like the proponents of unhandled rejection reporting by default have pigeon holed promises into a very specific usage pattern. Unfortunately, this usage pattern does not allow one to fully leverage the power that promises bring. It is unrealistic to write a truly promise-driven application when you have to have code like this:

constructor() {
    this.foo = doStuff();
    this.foo.catch(...);
    this.bar = thirdPartyLibrary(this.foo);
    this.bar.catch(...);
    this.zip = this.method(this.foo);
    // don't need to catch here, this.method sets up a catch
}

method(somePromise) {
    somePromise.catch(...);
    let temp = new Promise(...);
    temp.catch(...);
    return somePromise || temp;
}

The point that the above example is making is that it is very difficult to discern provenance of every variable, especially in JavaScript, so I have to put guard catch clauses anywhere I see a new promise entering my system and, unless I assume third party library authors are guarding everywhere as well, then I have to guard against all of my return promises (they may be given to a third party library) and all of my incoming promises.

I understand that debugging code is hard, I don't think castrating an excellent feature is the right solution to that problem though.

Looking at other languages with promises/futures, which ones treat unhandled rejections as errors like this? I believe Dart does, but the others I have looked into don't appear to. C++11 doesn't, Scala doesn't, .NET doesn't, I don't believe Java does, Python doesn't appear to, I'm not trying to appeal to authority here, but I believe if one wants to go against everyone who has gone down this path there should be a very strong reason for it, and I am not hearing that in any of these discussions.

That all being said, a choice does need to be made as to the direction of JavaScript. If JavaScript wants to be seen as an enterprise application development language then it needs powerful standardized asynchronous tools such as Promises (as implemented in other languages, catering to veteran developers looking to author complex applications). If JavaScript wants to remain a scripting language for the web, then the benefit of making it harder to screw up probably outweighs the advantage of powerful programming constructs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants