4 ms·
The problem with discussions of promises vs ... is there's usually a great deal of misunderstanding about what problem promises are solving: enforcement of the
by eldude 13y ago
The problem with discussions of promises vs ... is there's usually a great deal of misunderstanding about what problem promises are solving: enforcement of the callback contract. I could go on for hours about the subtleties in the conversation, but the gist of it boils down to this:
Node callbacks have a strict but entirely unenforced (in userland) contract:
The contract:
* Callbacks should be called OAOO
* Never throw (explicitly or implicitly) after origin tick (aka pass post-tick errors to callback)
There is also an increasingly common informal contract:
* Never throw ever (aka pass ALL errors to the callback)
But there are a couple of issues that get in the way of successful enforcement of this contract:
* When external libraries violate this contract (library A calls library B,
but library B violates the contract in a way that makes it difficult for
library A to easily maintain the contract)
* Asynchronous errors
* Crash-worthy exceptions
Inevitably, this all continually comes back to error handling over and over again, with the core issue being that node.js has 2 divergent methods for error handling: exceptions (try/catch/throw) and error passing (callback(err)). Exceptions requires opt-in error handling (crash by default) while error passing requires opt-out error handling (crash on demand) "and never the twain shall meet."
Promises attempt to resolve this by coercing all exceptions to error passing, but not all exceptions are safe to be passed. Specifically, any exception originating from core that is not an invalid argument exception (think 4XX vs 5XX class errors) cannot safely be caught / coerced / continued on. Also, not all errors passed to callbacks are even theoretically passable as the unofficial policy of core is that core callback errors are only distinguishable from exceptions in that they occur after the origin tick. In other words, they are equally crash-worthy and equally non-crash-worthy.
So promises are an improvement because when used pervasively, they enforce the formal and informal callback contracts, while providing a continuable-like representation of the value that can be passed around.
This benefit of contract enforcement is entirely independent of the specific API implementations.
The stepup[1] library trivially accomplishes contract enforcement by integrating the async trycatch[2] module without using promises or requiring pervasive usage. I've used them both for years now in various professional projects, with trycatch in use here at LinkedIn. Additionally, stepup could easily return a continuable to support a passable value representation.
Async generators will solve most of this though, allowing node.js to ditch the error passing for exception handling, but since Error subclassing and typed catches are basically not supported in node.js the language keeps getting in the way of a satisfactory complete solution. This is a whole other discussion.
In conclusion, error handling in node.js is an undesigned mess of which the core contributors don't even seem to fully understand or be aware[3,4]. FWIW, node.js' crash on error design is a DoS liability, which spion addresses in the article, and which LinkedIn uses trycatch to avoid.
[1] https://npmjs.org/package/stepup https://npmjs.org/package/stepup
[2] https://npmjs.org/package/trycatch https://npmjs.org/package/trycatch
[3] https://github.com/joyent/node/issues/5114 https://github.com/joyent/node/issues/5114
[4] https://github.com/joyent/node/issues/5149 https://github.com/joyent/node/issues/5149
- spion 13y ago> Promises attempt to resolve this by coercing all exceptions to error passing, but not all exceptions are safe to be passed. Specifically, any exception originating from core that is not an invalid argument exception (think 4XX vs 5XX class errors) cannot safely be caught / coerced / continued on. Also, not all errors passed to callbacks are even theoretically passable as the unofficial policy of core is that core callback errors are only distinguishable from exceptions in that they occur after the origin tick. In other words, they are equally crash-worthy and equally non-crash-worthy. This is not true. With promises, the wrapper of core functionality is left up to you. You can either write a crashy wrapper, an uncrashy wrapper, or a wrapper which picks whether to crash or not depending on the kind of error. I wrote more about this here [1] Most default wrappers provided by promise libraries catch all synchronous errors but don't do anything with asynchronous errors. So far, this seems to be a fine default -- most unrecoverable, state-corrupting thrown errors in node core are asynchronous, and all the invalid argument errors are synchronous. But even if that weren't the case, it would be a simple matter to write a more specific wrapper. [1]: https://github.com/petkaantonov/bluebird/issues/51#issuecomment-31125928 https://github.com/petkaantonov/bluebird/issues/51#issuecomm...
- eldude 13y agoYou're talking about the core/userland boundary beyond the origin tick, and we're both right. Promises coerce caught exceptions occurring on the origin tick to errors (bad), while allowing non-caught async errors to be handled in a custom manner as you point out. The comments following your linked comment address this nuance. I agree with Raynos that the core of the issue is as I point out here, 2 divergent incompatible error handling mechanisms, with both of them fundamentally broken: * error passing fails because we don't have CPS due to lack of Proper Tail Calls * throwing fails because we lack async try/catch or at least with async generators we lack a performant try/catch or with bluebird-like optimizations (hacks) we lack typed catch and Error.create The latter is far closer to a consistent error handling pattern than the former, which promises implement (poorly and verbosely IMO). Additionally, async generators also nicely address the issue of slicing your userland stack away from core, so you get a two for one.