On Monday, January 21, 2013 9:28:58 AM UTC-5, Kurt Blackwell wrote:
Most errors in JavaScript are thrown new Error()s, but I thought a promise named it's failure a "reason" because it could be something else explaining the failure? Is it unusual for a reason to be a string, and the expected value to also be a string? It's unreasonable to expect cleanup code to tell them apart.
Yeah, a reason can be anything, just as you can throw anything. In practice, people tend to use new Error() for rejections, just as they tend to do the same when throwing. I think it depends on the situation as to whether it's reasonable or not to be able to tell them apart. People do it.
There's a few problems here which need solving:
- Default reporting of errors that reach the end of a chain of promises.
- Allowing custom handling/logging of errors, possibly including chaining to a foreign callback.
- Reporting unhandled errors that the user might have missed because of an bug when chaining promises.
- finally-block cleanup code.
I don't think .always() currently does any of these properly. If you give it a proper error parameter this will achieve (2) and (4), assuming the users remembers to re-throw errors they want to continue to propagate. If you remove the return value, it will solve (1) and (2), but I can't use it for clean-up code anywhere other than the very end of the program.
Right, always() was never really intended to solve any of these. For better or for worse, it was simply meant as a shortcut for then(f, f), and it's a common idiom across many promise libs. I only really use it at the end of a promise chain--maybe that's a clue that it needs to change :)
For backward compat, I think always() as it exists today will need to stick around, but we could potentially work on devising some new API that solves at least a few of these problems.
I'm not sure what #2 means, could you explain a bit further? Do you mean allowing a side-effect without changing the value/outcome of the promise chain?
For #4, implementing a true finally-like behavior seems pretty tricky (impossible?) given the behavior of finally that allows a previous return/throw to propagate if a new return/throw is not encountered. Since promises allow undefined as a legal return value and functions implicitly return undefined, it's impossible (barring function.toString(), of course!) to distinguish between a function that intended to return undefined, and one that intended not to return anything at all.
Maybe if we put some constraints on it, there is something implementable and useful--like always propagating the previous result. I'm totally making this up, so I have no idea if it's what we'd really want, but maybe it's good starting point for discussion:
sortOfFinally: function(onFulfilledOrRejected) {
return this.then(
function(value) {
try {
onFulfilledOrRejected();
} catch(e) {}
return value;
},
function(reason) {
try {
onFulfilledOrRejected();
} catch(e) {}
throw reason;
}
);
}
One obvious problem is that it completely swallows all errors and the return value of onFulfilledOrRejected handler. Does this give you any ideas, though?
(3) is very difficult to solve for promises, perhaps impossible. The only solution I can think of is to set a process.nextTick() timeout whenever an error is caught. If the error isn't passed to another promise within the cycle, then the default error handler is used. However this will probably require developers to be particular aware of this behaviour.
I agree that 3 is impossible to solve without putting some constraints on the problem. It's basically a
halting problem for promises. We have a when/debug module that handles some, but not all cases. Using a setTimeout or nextTick to warn in some cases might be a good addition there. I've been wanting to refactor when/debug for a while (it has lots of little experiments in it), but haven't had time.
Some other promise libs use a done() or end() function to "cap" promise chains explicitly. This is similar to your idea above about removing the return value from always(), and works reasonably well, but I'm not a fan of it personally. I don't like adding artificial done/end() calls all over the place, which add refactoring hazards (forgetting to add them, forgetting to remove or move them, etc.).
Thanks for the thoughtful discussion so far!