Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Async/Await will make code simpler (patricktriest.com)
107 points by thmslee on Aug 21, 2017 | hide | past | favorite | 92 comments


I wish the await syntax was inverted; instead of waiting for an async function, let all async functions await by default. In other words, I wish this worked:

    let foo = myAsyncFunc()
    foo.bar()
Why? Because that's the common case, and the caller shouldn't care whether the function is async or not. If you have a non-async function that you want to change to be async, or the other way around, then you have to change every single call site.

The case where you want to store a promise or wait on multiple is actually the edge case, and they could have reused "async" here:

    Promise.all([
      async myAsyncFunc(),
      async someOtherAsyncFunc()
    ]);
Bonus functionality: If you do "async foo()" and foo() isn't async, the compiler could still ensure that it provided a promise.

Sadly, it's too late for this, and everyone's code will suffer as a result. It's way too easy to forget to add "await" to an async call, and doing so leads to failures that can be hard to track down.


> If you have a non-async function that you want to change to be async, or the other way around, then you have to change every single call site.

At least in the direction non-async -> async, that is clearly a good thing, since you are changing semantics a lot.

This code is always safe:

    x = globalObject.a
    functionWithoutSideEffects()
    gobalObject.a = x + 1
While this is not:

    x = globalObject.a
    await functionWithoutSideEffects()
    gobalObject.a = x + 1
> It's way too easy to forget to add "await" to an async call, and doing so leads to failures that can be hard to track down.

This a situation that could clearly be improved with better detection of these cases. Or maybe it should be completely illegal to call an async function without a keyword, so you have to do either `await func()` or `promise func()`.


I wish your comment gets upvoted to national news. Those async antipatterns are worse than the 'goto' statement. Time for programming languages to add the 'async' keyword on the caller side.


JavaScript often feels too verbose. That's why I like to use elm whenever it is possible.


I heard Doug Crockford talk about how he doesn't think async/await is that great an idea on a podcast a while ago.

His argument was that it's an unclean abstraction - it gives you access to 'features' of synchronous imperative syntax (lines in a function always execute in order, try-catch blocks, etc) but it remains conceptually and literally promises all the way down.

Therefore, all await 'calls' are really non-blocking at a global level, yet they appear blocking to the local lines of code inside the same async function. This is liable to cause confusion - particularly for beginners or occasional visitors to JS who don't fully grok or have the concept of promises top of mind - but really for everyone.

I've used async/await a fair amount in production code now (with babel) and while it does make some code a bit cleaner, honestly it is often to the detriment of understanding it when you come back to that code it in a few weeks. I've made plenty of stupid mistakes where the two 'faces' of the abstraction don't marry up, and it's frustrating.

More and more I'm inclined to just use promises, even when I have the choice of async/await - call a spade a spade and get on with your day. The article talks about promise chains getting complex and hard to read. Well, if this is the case, maybe it's your code or logic flows in general that need to be cleaned up, and changing the syntax to flatten the structures is actually just a sticky plaster over that.


Here's a great article somebody posted on HN awhile back, "What Color is Your Function?"

-- http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...

The proper abstraction is threads--i.e. a stack structure used for the storage of temporary values, which is shared by nested function invocations as well as similar structured programming constructs like if/else conditionals that together represent a singular thread of control through the program as it processes data and events. "Thread of control" is precisely what you're cobbling together with promises, async/await, etc, using clunky compiler constructs bolted onto the language model and implementation. The problem is that we conflate the implementation with the abstraction, sometimes by necessity (the legacy of the contiguous, so-called "C stack") but usually unnecessarily.

It pays to remember that some operating systems, such as some of IBM's mainframe platforms, IIRC, implement a thread stack as a linked-list of invocation frames, rather than a contiguous array. But it's completely transparent, as it should be. Async/await does the exact same thing, except it's not transparent as it should be. So now you have _two_ distinct, mutually incompatible kinds of functions solely because the implementation is unable to properly hide the gory details, which is ludicrous on its face.


I feel the conclusions of that article were pretty spot on. Having done a bunch of async programming in Go, and now picking up Node on the side, I can't help but agree that Go got this right. You have the simplest conceptual model (a synchronous one), with the runtime responsible for making it all run async. JS certainly seems to have come a long way, but it feels like the next step will be the most difficult one, requiring a serious redesign of the runtime itself.


I'm not fluent in Go but I read this blog post about asynchronicity in Go, https://www.golang-book.com/books/intro/10. Is the idea that Go will be synchronous in calls made with the go keyword only when there's some barrier put in place by a channel?


Only if you create a channel with no buffer: then it becomes blocking send or receive. Go channels block when they have nothing to do, either no room to send anything (in the case of a default channel with no buffer) or nothing to receive (when the channel has no messages).

It's not uncommon to use a select statement to allow work to continue (it may act like a loop) and wait to receive a message on a channel. This is the common pattern for handling timeouts: create a timer goroutine that will wake at a set time and send a message to a timeout channel, keep checking to see if work is done, and if the timer fires then cancel the select with an appropriate message (function call or return an error value).


You have two distinct function calls to know exactly which one is giving up control and which one doesn't. This allows for synchronization-free programming model.

And even if your green threads implementation multiplexes them onto a single thread where technically you don't have to do synchronization, having identical function calls makes it very hard to guarantee where your code yields and you still have to use synchronization primitives with all the problems that come with it.


light-weight multithreading that enters a scheduler loop when I/O would block is great, and I haven't seen any advantages of async/await compared to that, other than ease of implementation (you make the programmer or library author implement multithreading, rather than implementing it in your runtime).

Promises are at least slightly more interesting because they can functionally compose in various ways (but most of the time it's just a dozen then() calls in a row, and threading would have been better).


There are definitely patterns where a Promise API is more readable. You can implement a Promise API with or without separate threads, but if you execute it on a separate thread the number of library calls the Promise function can make isn't limited.

FWIW, I'd be careful about conflating issues such as how to handle I/O. Threads don't imply preemptive scheduling. Lua coroutines are implemented as threads, but that's the extent of things--there's no builtin scheduler or event loop nor features specifically directed toward implementing those things[1], just coroutine.resume and coroutine.yield. (yield can be called from any function at any depth as long as the call stack is on a coroutine thread, which in Lua is every stack except the main one.) And that's fine by me. The most fundamental issue from the perspective of the language design is about how to represent a thread of control that is consistent with the other structured programming constructs like function calls. A batteries-included event loop or a preemptive scheduler are nice to have, but notably they're much easier to implement (internally or third-party) and use if you have a thread construct.

[1] Debugging hooks notwithstanding.


I'm sure you can agree, that not having to worry about all kinds of races, contentions, deadlocks, wasting time on shotgun debugging, but still never really feeling like your program is reliable enough and about other "nice" things that come with shared memory multithreading is a huge advantage for any concurrent program.


Multithreading is not synonymous with preemptive scheduling. That's an entirely different issue. A chain of promises or async/await functions is no better in this respect than so-called green threading, and in many cases is worse than a cooperatively scheduled framework that provides more explicit control over the points at which thread execution can switch. For example, a system built on stackful coroutines where thread execution only occurs at explicit resume points, or a simple message passing model where execution changes only at the point a message is transferred. These are basically continuation passing style, except importantly the _state_ of recursive function invocations is completely transparent to intermediate functions, without having to explicitly pass around state or annotate your function definitions. In other words, no different than how you'd write any other function.

That's my point. The better _abstraction_ for all these things is a thread, no matter how you choose to schedule chunks of work or how you choose to implement things under the hood. A thread is just a construct that encompasses nested function invocations, and that construct is what promises and async/away emulate, except that that they leak implementation details and restrict the normal things functions do, like call other functions.


Here's the thing, if you can call functions that themselves can yield - you are in a shared memory multithreading model, where you can never guarantee for any function not to yield, so you have to use synchronization for that guarantee with all the same issues.


In a cooperative multi-threaded model you can only have data-races at function-call boundaries. For example: `x+=1` can never data-race.


Except if your programming language allows you to override the += operator.


You are proposing a situation where someone overrides += to specifically both call a blocking function and to not make it work correctly. I'm not saying it doesn't happen, but bad code is bad code regardless of your paradigm.

Though I must admit I've not done cooperative multitasking in a language with operator overloading so I can't say whether or not this is a problem in practice.


In can happen for example in Python with gevent.


> I've made plenty of stupid mistakes where the two 'faces' of the abstraction don't marry up, and it's frustrating.

Would you mind sharing any of these?


I'm talking really stupid, small, frustrating things, mostly at the interfaces. I read a return x at the bottom of one function and call the function elsewhere and try to use x, but oh wait no.. it was an async function so I've really got Promise<x>.

Generally it's just because of the leaky abstraction making very fast mental mapping of code a bit harder.


Another common mistake is accidentally not awaiting a function that returns a promise. That makes some really hard debugging (race conditions) as this issue is not caught even by TypeScript.


It's funny watch Js go through the motions C# did with TPL and Async/await.

The same exact arguments and pitfalls coming up


> honestly it is often to the detriment of understanding it when you come back to that code it in a few weeks.

Not in my case. I think async await makes everything quite clear BUT you have to understand promises and async await well. This stuff ia super tough and I needed a week or more. Then you can produce quite elegant code.


Maybe I didn't get this across but I do understand async/await and promises well, and that's kind of the point - it's still troublesome at times for me to understand at a glance once I'm out of the context of the code.. precisely because it's a slightly unclean abstraction.


I use the futures from node-fibers all over the place in my backend code. It has possibly only once caused me confusion over something. If you're writing your code right, it shouldn't cause confusion. You don't need to make your code ugly to understand when things are asynchronous.


I was super excited about async/await when it first came out. I hadn't really understood the point of Promises, but async/await looked simple and useful.

However, I recently started using async/await in TypeScript, and the result seems to be try/catch statements everywhere. Code using async/await seems to be more verbose and unruly than just sticking to Promises, which I now appreciate the elegance of much more (callee convenience/error delegation).

I think I'm just going to stick to Promises for the time being, until I see some hidden usefulness of async/await syntax (which could totally happen. It took me a long time to realize how awesome promises are).


> However, I recently started using async/await in TypeScript, and the result seems to be try/catch statements everywhere.

Why do you have try/catch everywhere? Unless you're actually going to handle the error, you shouldn't do anything as it will bubble up till it gets to a piece of the app that can handle it. For web apps a lot of the time that's a single request level handler to return a 5XX.

> Code using async/await seems to be more verbose and unruly than just sticking to Promises, which I now appreciate the elegance of much more (callee convenience/error delegation).

Eh? Besides the addition of the "async" keyword on the top level function, it's always less verbose.

Where I think things get a bit tricky is awaiting the composition of Promises to support parallel processing (as opposed to just 1) await foo() 2) await bar() ...). It's best to treat that like regular composition and simply await the result.


If you're awaiting/catching upstream, you don't need to wrap at the lower level, and it can be cleaner.

    const delay = ms => new Promise(r => setTimeout(r, ms));

    async function fooErrors() {
      await delay(100);
      throw new Error('I failed');
    }

    async function doSomething() {
      await fooErrors();
    }

    async function main() {
      try {
        await doSomething();
      } catch(err) {
        log.fatal(err);
      }
    }


This makes little sense. You don't need more try..catch blocks than you would normally have a catch promise clause.

Exceptions bubble up.


I'm finding that with client side async, the awaited behaviour is often external, heterogeneous and unreliable. When an operation fails, it typically requires a state-machine transition to a failed/retry state or fallback service rather than just bubbling up to a top level handler like a coding error i.e. there is something specific you need to do unlike typical exceptions.

Retrofitting a code base with async I concur that local try / catch has been necessary and has added code complexity and doesn't always feel like an improvement. In contrast, server side async has been much more elegant because the errors with internal async server operations are much more exceptional and don't require so much case specific handling.


You should make your API client layer be able to retry operations (and maybe track the status of a network connection) rather than write catch/then() manually.


It would be nice if it was that easy but if retry behaviour is dependent on the specific operation attempted and the error information in the response then you need some degree of local handling even if its just to prepare information for a generic handler.

Exceptions work best when the error is fatal for the local scope but responses from external services aren't like that. The general problem is that the dividing line between errors and information becomes too blurry - your error might only be information to me. A simple example is where something like axios will (by default) throw on 404 responses but an external api might use 404 to indicate a resource does not exist. If your app logic makes a decision based on this information, you will find yourself using exception handlers for control flow despite not experiencing any actual errors.


You can abstract your try / catch logic to a higher level to fix this easily. make all promises extend from a base promise function that either returns a result, or null (or an error, you can make it more complex). Then,

    let data = await customPromise()
    if(!data) return
this would be better in my opinion that try catch everywhere. Inside that higher order promise you can catch and handle errors.


This sounds confounding to me as a reader - you're telling me that exceptions will intentionally be handled further from where they're raised?


That is the benefit of exceptions over return values as usual in C.


hmm were you previously using Promise.catch for flow control? It's basically as bad as using exceptions for flow control, just with with a different set of bubbling / propagation problems. Unless you're dealing with truly exceptional conditions you should not be throwing inside your promises, thus no need for try / catch when converting them to async/await.


Rejection is turned into an exception with "await", though. And rejection is perfectly normal with, say, network I/O.


Well yes, that's a reasonable use case. Personally I'm not convinced that all 4xx responses should be rejections, but regardless of that, you should have about as many `catch` blocks with async-await as you have with promises.


I'd rather user try/catch than endless chains of then().


andThen()...andThen()...andThen().andThen().andThen();


Though the article doesn't mention it, the "alternative" is Reactive programming (via RxJS) I think for the typical UI application developer, async / await can provide easier readability and debugging, but at the cost of some expressive power and conciseness.

Now that chrome supports async / await in the debugger, it's almost certainly the best choice, compared to promises and callbacks.

In the redux world, you can see this by comparing redux-observable [1] (reactive / rxjs) with redux-saga [2] (generators)

Rxjs requires a deeper understanding of Reactive programming. Once you understand it, you can write very powerful expressions in a few lines. But debugging is tough and it doesn't translate well for your fellow developers.

[1] https://github.com/redux-observable/redux-observable

[2] https://github.com/redux-saga/redux-saga


I think it's worth noting that async/await is a bunch of sugar around promises... All async functions return a Promise... and all awaits await on a result or promise resolution. and Errors will bubble out.

This is helpful as many times I'll write a function that simply returns a promise to wrap around an older callback style function. I know I can promisify, but this doesn't always work as sometimes the methods need their context.

As to redux, frankly, I find redux-thunks + async functions to work pretty well together until you need more.


Only downside I've found with this is that Observables feel like more of a pain to test, since your logic gets more tightly coupled to your I/O. Or at least there's more complexity involved in the relationship between I/O and data manipulation. I used them for a Node project via RxJS and ended up just switching back to promises, as it wasn't complex enough of a project to really see much of a benefit from Observables.


The only difference between promises and observables are multiple-emission & cancelation, they shouldn't be any more complicated to use or test than promises. Often I find the main complicated part is the source, everything else is just filter/map functions, sometimes to more streams. All of these individual units are more easily described/tested in comparison to the entire chain (and even then you are only caring about the ends of it).


Along with that, generators (redux-saga) are amazingly simple to test.


I go back and forth on async/await. On the one hand, it is utterly brilliant. On the other hand, it seems like the final epicycle, trying to fit a theory of circular geocentric orbits (call/return) onto a real world of elliptical heliocentric ones (asynchronous programming).

So yes, it will make code easier, but I fear that will only serve to prolong the dominance of what is arguably the wrong programming model/architectural style. Or more precisely: an insufficient programming model/architectural style (it is great for a lot of things, just not for all).


The way I look at it is as just another way of manipulating continuations, alongside conditional, loops, call/return, generators, coroutines, exceptions, etc. Even in some other hypothetical asynchronous paradigm, all those patterns still occur all over the place, so making them all composable with each other is a win. (Credit to http://blog.paralleluniverse.co/2015/08/07/scoped-continuati... for describing it this way.)

The surface syntax could maybe use some work- maybe make functions generic across asynchronicity, maybe swap `await` to being the default and explicitly mark the case where you want a reified Promise instead, etc. But in the end it's another valuable control structure.


await/promises/etc mostly exist to solve the problem that JavaScript doesn't have threads so it can't wait for callbacks.

About JavaScript, many other languages have had async/await for a long time. I have no idea why JS made such a huge deal of promises, I guess they're better than the callback hell before. Of course, in most languages using async isn't nearly as important for performance because they have thread pools.

Some interfaces aren't and won't be asynchronous (like Linux file IO) so eventually JS will support proper threads and we can stop talking about how great asynchronous programming is (it isn't).


await/promises/etc mostly exist to solve the problem that JavaScript doesn't have threads

async/await was popularized in C#, which does have threads.


Maybe promises come before async/await, in an evolutionary sense. Same way that for-each loops seem to precede generators / yield. C# added Task before the syntax sugar of async/await. Java currently has Future, and I expect async/await to finally show up in about 10 years time...


Just wanted to write that ;). Despite being C# developer and often smiling at the state of the Java language, I think Java will get async/await faster than in 10 years. I am more optimistic about it.


No! Async/await is a horrible construct. Keep it away from Java.


Node has threads in C++ for that sort of thing. Async programming is a big deal for performance. That's essentially the main reason Node is any faster than Python. If you use fully async Python on uvloop, you can get comparable performance.


It is ultimately a failure of language and runtime that programmer has to manually specify where he wants to make asynchronous vs. synchronous functions to get the optimal performance.

This blog posts elaborates on that better then I could do here, so I'm just going to link to it: http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...


> that programmer has to manually specify where he wants to make asynchronous vs. synchronous functions to get the optimal performance.

programs aren't just pure computations. There are plenty of times when you want a specific event to happen at a specific time (as in, wall-clock), and plenty of times when you don't care when something computes as long as you end up getting a result at some point.


Yes, but that should be call-time distinction, not a function declaration distinction. This is briefly mentioned at the end of the linked article.

Edit: as other poster mentioned, async/await and promises also don't help with precise wall-clock time but that is entirely different matter.


You don't get reliable wall clock time unless you're working in RTOS. In a threaded OS everything in userland is async to an extent. In this school of thought, having to specify that something should be async manually could be seen as a failure of the language.


on recent good hardware there is no problem being around 1ms accuracy.


>That's essentially the main reason Node is any faster than Python.

v8's JIT is many times faster than Python in CPU bound tasks without any asynchronicity involved.


Python also has async/await


>So yes, it will make code easier, but I fear that will only serve to prolong the dominance of what is arguably the wrong programming model/architectural style.

Nested callbacks and even Promises are not the "right model/style" by any measure. Even 30+ year old languages had better answers to asynchronous programming than that.

Nested callbacks is so backwards its like writing in assembly. Only people whose first exposure to asynchronous programming was Node think it's a valid programming style.


> Nested callbacks is so backwards its like writing in assembly. Only people whose first exposure to asynchronous programming was Node think it's a valid programming style.

It depends on precisely what structures you include in your definition of "nested callbacks" here. I think that the callback and errback chains on the Deferred object in Twisted is basically the perfect abstraction for the flow control they represent.


> Nested callbacks and even Promises are not the "right model/style" by any measure.

Of course not.


I really don't see the need for such a syntax. If I say:

  Foo foo = someFoo();
  ...
  String content = foo.getContent();
I really don't care if foo was returned when I called someFoo(), I only care that getContent() is not attempted until after foo is defined.

There's little reason for me - in this situation at least - to need this async functionality to be explicitly stated.

The biggest problem I see is a when you use this functionality in a loop of independent executions (when the results of any given loop don't depend on the results of prior loops):

  for(Foo foo: asyncFoos()){
      Foo foo = someFoo();
      ...
      result.add(foo.getId(), foo.getContent());
  }
The problem here is that the use of a for loop will introduce latency, potentially dramatically increasing execution time.

Now you can either replace this "for" with some sort of "for-each" type of block, or you can go async all the way and treat "for" as "for-each" and any referenced result of a prior iteration as yet another async value.

That covers sets and singletons, I imagine that any other situation that needs to be dealt with can also be covered on the compiler side of things with only minimal changes to syntax.


I personally think that the syntax is great considering it's being added to an existing language. await/async allows you to opt-in to having your code behave in a synchronous-like manner, when you want it to.

One of the best things about JS/Node is that you can wait on I/O from different sources at the same time and since the async/await syntax is just sugar over Promises, you can do things like:

  const [user, notifications, messages] = await Promise.all([
    getUser(),
    getNotifications(),
    getMessages(),
  ]);
Instead of having to wait on I/O sequentially like:

  const user = getUser();
  const notifications = getNotifications();
  const messages = getMessages();
if awaiting were implicit.


It's usable, it's powerful and it's not overly verbose - but I'd contend it's adequate rather than great.


You may not care, but the computer does!


Perhaps it does, but my point was it does not need to. The only thing that needs to be ensured is that calls to the object are not issued before the result is returned and assigned - for all code that occurs between the call to get the object and the first actual access of the object, it does not matter.


Why doesn't the computer do it for me?


Agreed.


I really like the succinctness of async/await, and I feel like it's the last step of a long journey. The single threaded callback based style of Javascript has always been a mixed blessing. It has allowed for great performance, and it gives better control that having to juggle threads, but it was all to easy to get into deeply nested callback hell.

If you see design patterns as indicators of language smells -- and it's a useful perspective IMHO -- then in this case callbacks were the smell and the various promise libraries were the design patterns. It's a nice thing that promises and their async/await sugercoating have fairly rapidly made it into the core language.

Now, for the one thing that I don't like about async/await: it's deceivingly simple and it's bound to fool both novice programmers and programmers arriving from threaded languages. If you don't understand the underlying concept of promises, you'll easily end up writing suboptimal code (e.g. executing stuff in sequence which would be better executed in parallel).

Still, a net win.


same example with async control flow library and node style callback conventions

function getUserInfo(callback) { async.parallel([api.getUser, api.getFriends, api.getPhoto], callback) }


Even with a library like async there will be so much boilerplate, since you need to handle (or pass) errors every step of the way.

With async/await you only need to deal with errors at the level you actually care about them (using the language build in try/catch block).


In the example above, any error will be passed to the callback sent to getUserInfo - i.e., where you care about it.


Agreed there there is boiler plate - but with use of live templates in webstorm (or snippet in VS) - I find it getting easier.


    function getUserInfo(cb) {
      Promise.all([
        api.getUser(),
        api.getFriends(),
        api.getPhoto(),
      ]).then(
        ([user, friends, photo]) => 
          cb(null, {user, friends, photo}),
        cb,
      );
    }


I do this with so many libraries now. especially in the react-native ecosystem.

I just prefer node style callbacks, so I wrap libraries with promise based APIs with callback based wrappers.

hopefully i'm not the only one.


Async/await can be used to implement coroutines, but IIRC only if you wrap all of the code that might be called in async/await as well, and this catch makes them practically useless. Why not just provide proper coroutines?


Because people don't need coroutines, just a simpler way to write async code than callbacks and manual promises.

While we're at it why coroutines? You can implement coroutines, generators, even try/catch and return given continuations [0].

[0]: https://curiosity-driven.org/continuations


Not a gigantic fan of async/await for the reasons that others have mentioned here, but that said it provides the only sane out-of-the-box way for JS to handle loops that contain callbacks/promises.


Anyone else bothered by the utter lack of semicolons in the example code? :D


Yes It's like reading English without periods It's frustrating


If you've got a halfway decent linter, semicolons are just clutter. https://eslint.org/docs/rules/no-unexpected-multiline is the sort of thing that makes semicolon free style practical.


There is one situation where semicolons are important - when concatenating multiple script files & using IIFEs.

This subtlety is being obsoleted by ES modules & the build tooling around them, but it is a nasty bug that has sucked many an hour away from frontend devs whenever it is encountered.


Let go and let god. :D


I much prefer observables to tasks...


not too long ago, async used to be the "thing".

and now sync is the new "hot stuff".

a bit ironic but at the end of the day, simplicity always wins.


try catch try catch try catch try catch try catch try catch try catch try catch


better than then().catch().then().catch().then().catch().then().catch()


Or, you know, a single try catch. Or several of them. At any level you like and fits the problem. And no lost exceptions.


You're right and async await is much nicer but all the try catch blocks are slowly getting to me




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: