4 ms·
"Pyramids" are not that useful for complex flows, just as synchronous-looking code, but for something as simple as your example they are equivalent in complexit
by zzzcpan 10y ago
"Pyramids" are not that useful for complex flows, just as synchronous-looking code, but for something as simple as your example they are equivalent in complexity, maybe even less complex, because they don't have additional implicit behavior introduced by async/await. Still, worlds better, than CSP with channels.
- WorldMaker 10y agoasync/await ends up being the less complex thing, my "simple" example is an extreme that sometimes but rarely happens. When was the last time you saw that in synchronous code? It happens, certainly but the synchronous default is to only handle what you can handle and then pass everything else back up the chain. The asynchronous default with async/await is the same, and you can rely on default error propagation in the natural default case: async function doTheThing() { await thing1() await thing2() } That just works, if there's an error in thing1(), doTheThing() stops at that line and returns the rejected promise to whatever called it. The callback world is clearly the more complex here, where all error handling has to be properly wired and there is no default handling. The node callback equivalent to the above is: function doTheThing(callback) { thing1(function (error, value) { if (error) callback(error, null) thing2(function (error2, value2) { if (error2) callback(error2, null) callback(null, true) }) }) } You can't forget either if (error) check or else errors will just silently be eaten/ignored. The "implicit behavior" introduced by async/await are essentially the same things you are used to in code without async/away, such as the way try { } catch { } and throw naturally work in synchronous. That alone should be considered a simplifying improvement.
- zzzcpan 10y agoFirst of all, exceptions are not a great way to handle errors, because they are implicit. There is no special keyword, like "await", to permit a function to throw exception, you have to guess an implicit behavior of each function. And you will have functions that never throw exceptions, functions that do, functions that lie about whether they do or do not. You will have to remember that at all times and be very careful to always use try/catch if unsure and so on. It's a big cognitive load to have. Most of the times the easiest and the only reasonable way to deal with errors is to be explicit about them. So, the equivalent to that code with callbacks is the one from your previous example and not the simplified one that doesn't handle errors. Not forgetting "if (error)" in this case will be exactly the same, as not forgetting "catch (error)" or "if (error)" if you handle errors properly in the first place. But these are just patterns and with enough consistency they don't introduce much cognitive load. What does is implicit behavior and you don't have one with callbacks in your example, but you do with await. It's not as bad, as with threads, but still a little bit worse, than with callbacks.
- WorldMaker 10y agoI'm sorry you don't like exceptions. It's a pattern that's pretty well understood at this point, has been in the JS language for quite some time now and other languages for even longer. «So, the equivalent to that code with callbacks is the one from your previous example and not the simplified one that doesn't handle errors.» No, it isn't. It really isn't. The callback version isn't doing any sort of handling of errors, it's just laddering them, passing them back up the chain. The async/await version is doing the same automatically, wiring together for you the error handling to pass errors back to calling functions. Even if you did want to handle every possible error/exception that could be thrown in doTheThing(), you don't really have to go all the way to the "parade" version, as only one try/catch should be sufficient in doTheThing(). The issue here is that where you see error handling as a cognitive load, the general best practices for error handling have mostly leaned to the goal that exceptions are for actual exceptional cases and catching exceptions should be left to cases where the user can actually fix the exceptional situation (rare) or logged somewhere appropriate in a global handler. If you are unsure if you should handle an exception, don't handle the exception. Let the calling function catch, or let it continue bubbling up to whatever global exception handlers you want to set, or even just leave them for the browser's Unhandled Exception Handler and Unhandled Rejected Promise Handler to spit out debug information in its dev tools console. Don't bother remembering anything, just let your tools do their job. The difference between catch (error) and if (error) in the callback example is that if you "forget" catch (error), it bubbles up to your browser's handlers, but if you forget if (error) it doesn't stop execution, it doesn't bubble up to your global toolkits and exception handlers and it doesn't bubble up into your dev tools debuggers. (One thing I realized in writing this: due to my rustiness in callback coding I forgot that they should be if (error) return callback(error, null) to properly stop execution, which you want in the default case of not handling the error but passing it back up the chain. Small mistake that could cause debugging headache and adds to my point that the little things that are easy to forget that you always must do in the callback world can have a major impact on debugging...)
- z3t4 10y agoDon't forget that for every await - you or preferably someone else has to write the Promise boilerplate!
- WorldMaker 10y agopromise-ring [1] is all you need for Node style callbacks. (Bluebird and a few other Promise augmenting libraries also have similar tools, but promise-ring is really all you need.) Eventually, too, Node and Electron and Cordova and everything else will catch up to the fact that Promises are native in ES2015 and the APIs will eventually migrate. But the thing about it is toi, async functions also return promises, so not every await require "Promise boilerplate", as you will presumably be using other async functions that produce their own promises. [1] https://github.com/DavidAnson/promise-ring https://github.com/DavidAnson/promise-ring