4 ms·
I appreciate that the author is trying to illustrate something with an intentionally contrived example, but there is really no need for "await" in his "chain()"
by gbuk2013 7y ago
I appreciate that the author is trying to illustrate something with an intentionally contrived example, but there is really no need for "await" in his "chain()" function:
function chain () {
(function loop (i) {
if (i > 5) {
// set state to failed
return;
}
check().then((result) => {
if (!result) {
return loop(i + 1);
}
// set state to finished
}).catch((error) => {
// set state to failed
});
}(0));
}
This is one of my pet peeve with proponents of promises actually. They come up with convoluted examples to argue against callbacks but it's not the callbacks that are a problem, but the person writing the code.
Actually, I would argue that promises really made no sense until "await" became available because they introduce extra complexity and (small) performance penalty for no added benefit. Worse, it encourages less experienced developers to write code in a serial manner in situations where this is not necessary (a much bigger performance problem).
Even with "await", the only time when it is really useful is when there have to be a several asynchronous operations that must happen serially because they use the result of the previous call. It depends on your field of work, of course, but in my experience these situations are really not that common.
Some time ago I wrote an article comparing performance of callbacks vs async/await but it is also an example to show that callback code does not have to be more tedious to write:
https://gir.me.uk/posts/node-8-async-await-performance-test.html https://gir.me.uk/posts/node-8-async-await-performance-test....
- koolba 7y ago> This is one of my pet peeve with proponents of promises actually. They come up with convoluted examples to argue against callbacks but it's not the callbacks that are a problem, but the person writing the code. While I agree the author’s examples are convoluted, the async-await code is much easier to read, write, and validate. Why bother tracking state and having to keep tracking of JS lexical scoping when you can write "boring" code that will be just as fast for the tiny iteration counts you'll be dealing with? > Even with "await", the only time when it is really useful is when there have to be a several asynchronous operations that must happen serially because they use the result of the previous call. It depends on your field of work, but in my experience these situations are really no that common. Concurrent tasks are more pleasant with await as you can combine it with Promise.all(...) to get efficiency and legibility: async function doStuff() { // Fetch things concurrently const [ foos, bars, bazs, ] = await Promise.all([ getFoos(), getBars(), getBazs(), ]); // Do stuff with them... }
- gbuk2013 7y ago> the async-await code is much easier to read, write, and validate. With the exception of necessarily serial code that I referred to, this statement is not true in my experience. Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. In the real world each call may need different logging or even error handling logic and that is when promises really turn into a rats nest. Validation, i.e. tests, are really the same regardless which approach you are using. I do agree that async/await made promises actually usable, as I said above. For running a bunch of asynchronous functions that do the same thing the semaphore + collector pattern works just fine and is not much more verbose.
- Klathmon 7y ago>Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. async function doStuff() { try { // Fetch things concurrently const [ foos, bars, bazs, ] = await Promise.all([ getFoos().then((res) => { console.log(`getFoos done with ${res}` return res })), getBars(), getBazs() .catch((err) => { // handle only the error in getBazs }), ]); // Do stuff with them... } catch (err) { // handle any errors } } it still isn't what i'd call "pretty" code, but it's simpler for me to read and comprehend than doing most of those things without async/await or Promise.all or other helpers like that.
- koolba 7y agoI was about to write up that exact example of the nested handlers.
- Klathmon 7y agoI actually love slinging promises like that, but I tend to avoid it in most cases as there are MANY devs that would be somewhat rightfully frustrated at the code I just wrote. As always, who you are working with matters a LOT, and if I were on a team where a good portion of the devs weren't comfortable with that style of code, then I'd use more verbose ways of doing it that are more inline with what they are expecting. But on the rare occasion that the complexity necessitates the advanced promise stuff, or in jobs (like my current one) where all of us devs are super comfortable with promises being used like this, then it's a dream to work with them!
- mschuetz 7y ago> It depends on your field of work, of course, but in my experience these situations are really not that common. Very common in my field. A single function that loads a file, parses metadata, then transforms the binary data into a different format can consist of 2-4 async calls that need to be executed in order. Await is incredibly helpful here, and makes code much more readable compared to the chain of then statements we previously used. Even something simple as the fetch API to load a json file consists of two async calls in a row: const response = await fetch('http://example.com/movies.json'); const myJson = await response.json(); https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/U...
- gbuk2013 7y agoAnd your error handling? ;) fetch((err, res) => { if (err) { // yay - error handling! :) } const myJson = res.json(); // do stuff } What's so unreadable about this? Even if it is 4 calls it would still be readable because if you have any sizeable amount of logic you should be refactoring it into several functions anyway with the above function gluing them together. Much easier to write tests for too!
- mschuetz 7y agoAs far as I can tell, json() also needs to be treated as a promise, and fetch doesn't take a callback as an argument Wouldn't the correct non-await version look like this? fetch(url).then( response => { response.json().then( json => { // do stuff }); }); This is already much worse then the await version, without any error handling yet. And if I need to call another promise in order, I'd need yet another level of callback. Instead of two easily intelligible lines that could be wrapped into a single try/catch, we now have 4 lines and two new scopes/levels. Why would I ever use this over await?
- Klathmon 7y agoyou can chain them and avoid the nesting. Anything a .then handler returns becomes the value of the next one like this: fetch(url) .then((response) => response.json()) .then((json) => { // do stuff }) Promises will wait until resolved, normal values will call the next handler "right away" (there's some nuance here and some edge cases about what "right away" means, but for the most part you never need to think about that) And if you want to do more things and handle errors, it becomes pretty simple as well: fetch(url) .then((response) => response.json()) .then((json) => { if (!json.user.id) { throw new Error('user id not found') } else { return db.sql('SELECT * from users where id = $1', [json.user.id]) } }).then((user) => { return response.send(user) }).catch((err) => { // any error thrown at any point during the chain will trigger this catch return response.error(err) }) I still completely agree that async/await is still better in this case, but then throw some more wrenches into the situation like wanting to handle multiple promises at a time and you start to see where using "raw promises" really comes in handy. Like this: try { const res = await fetch(url) const json = await res.json() if (!json.user.id) { throw new Error('user id not found') } const [ userObj, userAuthLevel, someOtherStuff] = await Promise.all([ db.sql('SELECT * from users where id = $1', [json.user.id]), db.sql('SELECT * from otherStuff where userId = $1', [json.user.id]), fetch('https://other.stuff/and/things') ]) return response.send({ userObj, userAuthLevel, someOtherStuff }) } catch (err) { return response.error(err) } or say there's an expensive call that you can start BEFORE the first fetch, but still need to wait on later (ignoring most other stuff for simplicity): // notice there's no await... // we can kick off the request now, but not wait for the result until later const someOtherStuffPromise = fetch('https://other.stuff/and/things') const res = await fetch(url) const json = await res.json() // do other things here // finally wait for the promise to resolve here. const someOtherStuff = await someOtherStuffPromise
- BenoitEssiambre 7y agoI agree. Promises solve superficial aesthetic problems that only beginners trip on and unnecessarily add dangerous hidden state and corner cases. You can further simplify that chain() function by removing promises altogether: function chain () { (function loop (i) { if (i > 5) { // set state to failed return; } check((err,result) => { if(err){//set state to failed return;} if (!result) { return loop(i + 1); } // set state to finished }); }(0)); } Callbacks are much simpler and less error prone: https://medium.com/@b.essiambre/continuation-passing-style-patterns-for-javascript-5528449d3070?source=friends_link&sk=976fb25ca6c15eba3a4badcf55ba698e https://medium.com/@b.essiambre/continuation-passing-style-p...
- hajile 7y agoCPS is a very bad idea in JS at present. CPS in scheme works because proper tail call optimization exists, so your call stack doesn't explode. In current JS, the only way to make that work is performing a nextTick or setTimeout on every single function which trashes performance of simple things due to exiting your code back to the event loop all the time. <stands on soapbox> There's a special place in hell reserved for companies that deviate from standards because of petty disagreements. Proper tail calls have been a part of the JS spec for 5 versions now. They are implemented in Safari and in popular embedded engines like duktape. MS and Firefox refused to implement. They were in v8 behind a flag, but were removed. v8 devs pitched a fit about stack traces disappearing (even though they disappear across the event loop anyway). The webkit team showed that Chicken scheme's shadow stack works just fine and it's been in production for a long time now (without any issues I'd add). Instead, they insisted that they wanted to revise the standard to require marking tail-recursive functions. They also talked about decreased performance, but JSC team seems to have implemented it with a minimal performance impact, so it's definitely possible. More importantly, JS is a language to make development easy -- we have wasm in development for when speed becomes an issue and it's faster anyway (the label proposal would have also solved the issue by giving devs a choice). They then dropped their own proposal, but still simply refused to re-add the feature as per the spec in a bid to force a spec change. I have some understanding of not turning a feature on until a newer proposal is considered, but when they dropped that proposal and still refused to implement, they lost all credibility in my estimation. https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit/ https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-... https://www.more-magic.net/posts/internals-gc.html https://www.more-magic.net/posts/internals-gc.html <soap-boxing intensifies> I think this is indicative of a general "we know better than you" attitude on the part of the engine designers (especially the ones that sit on the spec committee). Another amazing example of this is the private variable proposal. NEVER has a JS proposal received such outspoken disapproval. Complaints range from the mostly insignificant "it's ugly" to the important "it goes against JS's core prototypical nature" to the very important "refactoring and maintaining this will be a nightmare due to dedicated syntax and JS being so dynamic", "it looks like a normal JS property that is hidden, but it in fact has almost nothing in common with normal properties even though the access syntax looks similar" or "this screws with inheritance, subclassing, destructuring syntax, etc". Instead of listening, they locked down all dissenting discussions on the proposal's github and are going full steam ahead. EDIT: some discussions are unlocked (see below for a decent argument summary). https://github.com/tc39/proposal-class-fields/issues/150 https://github.com/tc39/proposal-class-fields/issues/150 Decorators, pipe operators, tuples, immutable records, and slice notation are all well-understood ideas. Most don't require huge amounts of work to implement, but result in large quality-of-life improvements for developers. Most importantly, they aren't really controversial at all and most are easily transpilable so devs could start using them today (unlike private vars which either don't actually transpile correctly or create a mountain of unreadable garbage) Why can't we work on these things that have such wide user impact instead of forcing through controversial changes that only benefit a very few people?