49 comments

[ 6.5 ms ] story [ 90.9 ms ] thread
I don't get what's the point of his article. Promises and async/await are here for ages. Moreover it does not highlight issues with await such as if you forget to await a promise you've just introduced a subtle but in your code. Sounds unlikely? Check out the last snippet in the article (waitForHello(2000) missing await, doesn't do any harm in this case as there are no code after it but it's a hidden bug).
I also am confused by using the Pyramid of Doom as a pseudo rationale for needing this. Pyramid too tall? Use a function call. Nothing at all wrong with:

    function multiStepAsyncProcess() {
        hitServer(params, function() {
            multiStepAsyncProcessStep2();
        });
    }

    function multiStepAsyncProcessStep2() {
        hitServer(params, function() {
            ...
        });
    }
Now do it with error handling and recovery.
Moreover, async/await allows you to preserve multiple intermediary results between async steps - which is awkward in both promises and un-nested pyramids like the OP's:

(not even starting with conditions, loops and recursive calls that contain async steps...)

Async/Await:

  async function f(a) {
   var b = await foo(a);
   var c = await bar(a, b);
   return baz(a, b, c);
  }
Promises:

  function f(a) {
   var _b;
   return foo(a)
    .then(b => {
      _b = b;
      return bar(a, b);
    })
    .then(c => baz(a, _b, c))
  }
Callbacks:

  function f(a, done) {
   foo(a, b => bar(a, b, c => baz(a, b, c, done)));
  }
Callbacks untangled:

  function f(a, done) {
   var _b;
   foo(a, step2);

   function step2(b) {
    _b = b;
    bar(a, b, step3);
   }  

   function step3(c) {
    baz(a, _b, c, done);
   }
  }
<shrugs> ok

   function multiStepAsyncProcess() {
        hitServer(params, function(err) {
            if(err) {
                handleError(err);
                return;
            }
            multiStepAsyncProcessStep2();
        });
    }

    function multiStepAsyncProcessStep2() {
        hitServer(params, function(err) {
            ...
        });
    }
That just "fixes it" with indirection, which is the only tool you have with callback style.
Actually I characterize it as opting out of newly-introduced madness, and using the techniques we used pre-madness. Not a “fix” so much.
I can agree with that.

I think the problem with these sorts of discussions starts when someone dismisses promises, at which point one must assume that the person dismisses the problems that promises solve.

And I think that happens in the other direction too, when someone dismisses callbacks which suggests that they think that callbacks are intractable in all scenarios.

You are right. This has been a common error for me when writing code using async/await, but IMO it is not worse than forgetting to return promises in promise chains, so async/await is still a massive improvement over plain promises for me.
The problem with async await is that it is build upon promises, which sucked already from the start.

For all those Promises proponents out there ready to burn me down, this is how you do it in C:

  delay(1000);
And here the amazing Promises with await:

  var wait = ms => new Promise((r, j)=> setTimeout(r, ms));
  await wait(1000);
 
Is it really that difficult to at least abstract this wait function away in the Javascript language?
Yes it is. Synchronous calls monopolize the event loop so everything - including page interactions - freeze until the control flow finishes busy-waiting. Yay, single threading.
Everybody solves that by having multiple threads (either at the OS or runtime level), so you can run in parallel the stuff that don't conflict with each other.
(comment deleted)
(comment deleted)
Do you, as a user, want web pages to spawn multiple threads on your machine?
Well, first, you are completely ignoring the possibility of runtime threads.

Also, why not? Do you see any problem with that? As long as their numbers are limited, I don't see any problem?

On the other hand, well-known constructs as used for translating FORTRAN labels to switch statements have been around for ages. E.g.,

  function main(entrypoint) {
    switch (entrypoint) {
      default:
      case 0:
        // do some (...)
        // now drop the thread and wait for input (external)
        myInputFunc(function(result) { main(1, result); });
        break;
      case 1:
        // return to here from input
        var value = arguments[1];
        // now do something with it ...
        break;
    }
  }
  
  function myInputFunc(callback) {
    // in actuality this might be servicing a more complex UI
    document.querySelector('#inputField').oninput =
      function() { callback(this.value); };
  }
This way we were porting former terminal services to web applications back in the last century ...

(Maybe requesting additional server data by what would be called padded JSON nowadays via a hidden frame, because no AJAX. And, maybe, we would have rather used global vars for control than anonymous functions, but this another story.)

:-)

Promises do not suck at all. They save us from the callback hell. Never treat JavaScript as if it's C, they have fundamental differences.

The biggest difference is that JS is designed to run in an event loop inside a single thread. If you perform a 10 second blocking I/O your thread will freeze. You don't want your browser/tab to be unresponsive. Or in the case of a web server you want to accept incoming TCP connections as fast as you can.

You can achieve parallelism with threads but it's very tricky to scale. If you want a simpler, threaded, blocking programming model I suggest you take a look at Go.

Edit: Definitely read the original slides from Ryan Dahl's JSConf 2009 presentation, they perfectly explain the point of using JS: http://tinyclouds.org/jsconf.pdf

The callback style pseudo code they posted as an example is not a good example of callback style JavaScript code.

The first code posted is showing callback style with a nested loop over a directory listing to perform an image resize using graphics magick. Aside from the fact the author isn't actually calling any callbacks to indicate a complete event ( and won't be able to unless they have an async iterator ), there is a concurrency problem with usage of gm.

You really actually want to a run something like an async eachLimit in order to set the amount of open GM instances running concurrently or run the risk of running out of memory / cpu.

The second example using async/await doesn't mention anything about using async iterators either.

see:

http://caolan.github.io/async/docs.html#eachLimit

https://github.com/tc39/proposal-async-iteration

A bit OT, but may I suggest for a style guide to not use the shorthand, w/o brackets, in single-argument arrow functions, as in

  var wait = ms => new Promise((r, j)=>setTimeout(r, ms))
Here, the semantics of "ms" are not resolved by its lefthand side, but only by its righthand side (i.e., if this was anything but an arrow, "ms" would be a value assigned to "wait", while here it's the beginning of a functional construct, spanning over the rest of the line.) Opposed to this, using brackets for arguments anytime conveys the meaning much more clearly:

  var wait = (ms) => new Promise((r, j) => setTimeout(r, ms));
Edit: The amount of look-ahead introduced by the shorthand syntax is rather foreign to JS, as is the switch in syntax that goes with the number of arguments (as illustrated by the example). I'm not convinced that this was the greatest idea in the history of JS syntax ever.
Isn't that always the case? You always need to take into consideration the evaluation of the rhs. Consider:

   var wait = ms * foo()
And that's not even considering the complexity introduced by the comma operator. I think the arrow is the key to visually identifying a function, not the parentheses.
It's really about the arrow operator starting with "=", which is most likely the beginning of a chained assignment as in

  a = b = c = 0;
Also (as noted in the edit), I'm not the greatest of fans of the syntax switching with the number of arguments. In the example given, there are two arrow functions defined, each of them coming in a different appearance, due to the number of arguments used. Using always brackets, the nature of "ms" and it being used and passed as an argument would be more striking on first sight. Ease of understanding and readability aren't just luxury for a codebase.

(The real difference as for pragmatics is this: In any other case, anything on the righthand side of the first "=" is a value resolved on first execution whenever the code is hit, while in this case it's a parameter that becomes evaluated whenever the function defined by this assignment is called – and this relation isn't resolved without look-ahead. Possible errors introduced by a sloppy (mis)reading are numerous.)

> is most likely the beginning of a chained assignment

I wouldn't think so at all. I almost never do chain assignment in modern JavaScript but I write functions which return functions somewhat often.

I guess there's a possibility that someone quickly skimming code would misread => as =. It hasn't yet happened to me though.

> in modern JavaScript

You are probably right on this – but there's still old code around and ease of context switching may be a thing. Also, I guess, you can't be too defensive in your approaches, when setting up a shared codebase that may be around for some time. (With JS being extended on a yearly basis, we hardly can imagine what will be around a few years from now.)

Edit: However, also related to modern JS, another possible counter argument to mine: With the omission of terminating semicolons and frequent use of multiline statements determining the semantics has also become a question of scanning the next line anyway. Look-ahead is now a thing.

Some style guides enforce your proposal, but I prefer less noise where possible, especially for currying.

    const plugin = store => next => action => foo(store, next, action)
vs.

    const plugin = (store) => (next) => (action) => foo(store, next, action)
Should mention that Bluebird does this.

http://bluebirdjs.com/docs/api/delay.html

Yup, Bluebird has allowed for:

  await Promise.delay(1000);
For as long as I can remember. I'm surprised Bluebird isn't more widely-adopted; it's been way better than native promises for at least 2 years now. The transform-async-to-module-method plugin with Bluebird is the way to go.
Promises and Async/Await are just too slow. I'll stick with Callbacks.

https://kyrylkov.com/2017/04/25/native-promises-async-functi...

How is that possible ? Await doesn't have this effect in python 3 and v8 is faster than cpython.
Promises require creating and modifying objects that contain the promise state which plain callbacks do not.

The difference in micro-benchmarks can be huge: https://jsperf.com/native-promise-vs-callback

That example is a probably a worst case scenario because of how it is constructed, but on my MBP it still gets around 1M promises created and resolved per second.

For a typical web app or api you will not see any benefit in using callbacks over async/await in your application code unless you are consistently doing something that requires 1000's of promises per request.

Shouldn't we be more focused on which is easier to use though? Even if promises are slower, they probably aren't going to be a major performance issue, so I think if you find promises easier to use and less bug-prone, they are better regardless of preformance.
I don't think we should. I chose node over ruby because of performance. Those numbers are showing significant overhead for promises and async/await. That's a problem I don't want to inject into my latency-sensitive application.
> Shouldn't we be more focused on which is easier to use though?

In JS I think this depends a lot on whether you are writing client-side code or not. Prioritizing developer ease of use over end-user performance is a very bad habit to get into. But it matters less when writing code that'll run in Node.

(that said, I'm not at all convinced that the performance difference mentioned in this article matters a whole lot. It's not actually that significant.)

> Prioritizing developer ease of use over end-user performance is a very bad habit to get into

Why is that? Whenever you use a high level language, you are making development easier at the cost of end-user performance. Or using a foreach loop instead of a regular for loop, is usually a bit slower, but makes development easier and less bug prone. Premature optimisation is the root of all evil and all that. Obviously at some point you have to prioritise end-user performance, when making decisions that will have a big impact on performance, but when making decisions on the level of promises vs callbacks, I don't think end-user performance should be a priority.

Keep in mind that these microbenchmarks are run with N=10000, which mostly renders the 600ms total latency difference insignificant. Its also highly likely that the gap will become minimal over time.

If that's still a non-negligible difference, I don't see why one wouldn't just write a server in something like go instead. Node isn't the best choice for a performance-sensitive application.

Not to mention whatever task you’re actually doing inside the Promise will be an order of magnitude slower than settling the Promise itself.
"Programs must be written for people to read, and only incidentally for machines to execute"
The longer I see people debate the value of promises and their obscured use via async/await vs callbacks I am more assured of the value of simple callbacks. If you are experiencing callback hell you have bigger problems than callbacks. If you love promises great! But please don't force them on me or I'll find another api to use thank you.
I don't think any library authors care if you don't use their library.

But I feel bad for everyone that has to touch your code.

I haven't heard or read a lot of debate claiming that callbacks are superior to promises and async/await.

From the people I work with and everything I have read there is almost universal agreement that promise based code is easier to work with than callback based code and async/await is superior to both.

Callbacks do have superior performance, so if you are writing some really performance sensitive code then it might be worth using them, but like all optimization techniques that should only be done if the case truly requires it. Most don't.

Multiple async vehicles can and will coexist in JS. IMO there’s way too much FUD about “one async story to rule them all”.
(comment deleted)
I love async/await, but it's not because it's "aesthetically pleasing". It's because having execution jump around from the outer body to the inner anonymous functions fucks with all my intuition and, especially, with the way debuggers work.

If I want a series of steps to execute in order, I should be able to write them down in sequential lines of code, and it's, imo, a historical artifact that I was ever not able to do that -- because (oops), JS was traditionally conceived to have sequential lines of code execute immediately after each other, instead of blocking until they're able to proceed.

That is, it's not required that "single threaded language" meant "non-blocking execution" and thus required "callbacks and promises to do asynchronous behavior". It just ended up that way -- and we're finally (almost) free of it.

Thinking that it's a historical accident that everything is async in Javascript is a fundamental misunderstanding of async-everything.
Want to elaborate? I disagree, but I'm happy to be corrected. Anyway, what I said was that it's a historical artifact that JS has a single thread of execution, and by that I meant, "it was standard to do that at the time that JS was conceived, so that's how it was done". If you were inventing web browsers today I don't think you'd ever intentionally choose to do it that way.
I think jumping between functions makes JS fun :P It really clicked with me when I learned how closures work, and to use named functions.