promises-aplus / unhandled-rejections-spec

Discussion and drafts of possible ways to deal with unhandled rejections, across implementations
6 stars 0 forks source link

Background information #1

Open domenic opened 11 years ago

domenic commented 11 years ago

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

Terminology

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 commented 11 years ago

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

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 commented 11 years ago

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 commented 11 years ago

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 commented 11 years ago

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 commented 11 years ago

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 commented 11 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

@Zoltu I strongly disagree. https://blog.domenic.me/youre-missing-the-point-of-promises/

bergus commented 9 years ago

@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 commented 9 years ago

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.