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
[2] https://npmjs.org/package/trycatch