Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Thing of it as a different analogy. There are various chores to do around the house - some of them must be done synchronously (do the ironing - we don't want to leave the iron on). Some of them can be done asynchronously (run the washing machine). If you want to write out your tasks it might look something like this.

  LoadWasher() (sync) 
  washerRunning = RunWasher() (async)
  var cleaning = CleanKitchen()  (async)
  wetClothes = await washerRunning;
  LoadDryer(wetClothes)
  clothesToIron = await RunDryer()  // while we're awaiting we walk back up the stack and can continue cleaning the kitchen until the dryer is finished
  IronClothes(clothesToIron) // this is just happening synchronously
  await cleaning //last job to do so we need to make sure we finish!
Don't know if that helps, but I think it's an interesting analogy


That is a good example. It illustrates that async is a partial abstraction on the way to dataflow. It's not immediately obvious from your example what's really going on: whereas re-writing it as dataflow pipelines makes the intent clear:

    LoadWasher() | RunWasher() | LoadDryer() | RunDryer() | IronClothes()
    CleanKitchen()
where `|` is piping the output of one operation into the next one (as per *nix shell). The two lines are independent and so can be scheduled according to resources available.

Doing so makes the inherent dependencies trivially clear. It doesn't matter which are "long" running and which aren't: it's not possible to run the washer until it's been loaded. Using async/await conflates two things:

1. What's "long" running, for some definition of "long" (--> make it async)

2. What's dependent on what (await)

--

EDIT: fix formatting & added missing "RunDryer()" step


So you basically you'd have to rely on each of the function signatures, probably with some text editor/IDE by hovering over them, to figure out which one in the pipe list is Async? Or jump to each of them with ctags.

Then wrap it in a try/catch or pattern match the result to handle the various errors different errors...


> So you basically you'd have to rely on each of the function signatures, probably with some text editor/IDE by hovering over them, to figure out which one in the pipe list is Async? Or jump to each of them with ctags.

No, the point is you don't _need_ async. At least not as a language primitive. Instead, one expresses directly where the data dependencies lie. If an operation in a pipeline blocks, the runtime is at liberty to switch to another task. That already happens today, whether in language runtimes (e.g. Node) or the underlying OS. I don't need to tell the runtime "this operation might block". It already knows.

Instead, we've explicitly defined intent: when `RunWasher()` finishes, `LoadDryer()`. It doesn't matter if the washer takes one micro second or one hour; I can't load the dryer until it's done.

>Then wrap it in a try/catch or pattern match the result to handle the various errors different errors...

True, error handling is missing. But then it's not in the parent example either. The requirements aren't fundamentally different in either case.


Your analogy works as long as there's a machine doing the washing and drying.

If you replace the machines with people, it makes much more sense for the function definition to define what is and isn't synchronous. If you have to do all the chores yourself, just call the functions synchronously. If you have family members you can delegate some work to, you can call the functions asynchronously and wait for them to complete while you're doing your own work (or twiddling your thumbs).

Elixir allows you to do this very cleanly. https://hexdocs.pm/elixir/Task.html#content


Another problem with await is that you can't run in parallel without going down one abstraction layer. So it just add additional complexity as you need to know two layers. The hardest part with software is to not implement too many features.


My problem with this is if 'I' (or in code, the current program) actually loads the washer and cleans the kitchen, then these things run in serial.

If I do what I've always done in rust, which in use threads, I get lovely easy parallelization. Async only seems useful when your program doesn't do anything non trivial, but just dispatches to other things, as any one function that takes a long time blocks all other async functions until completion?


Yes, but you can run your blocking code on a thread which presents itself as a future:

- What is the best approach to encapsulate blocking I/O in future-rs? — https://stackoverflow.com/q/41932137/155423


The problem is that you don't always know beforehand whether you'd like to perform a task synchronously or not.


It’s easy to run an async function synchronously; just drop the `await` keyword and wait on the returned future.


And so all library functions should be async by default? And if everything is async, then shouldn't synchronous programming require the extra keyword instead of the other way around?


No. A function that makes no async calls has no reason to be marked async.


And if someone wants to add an async call later, all callers need to change too.


Only if the function "leaks" the async-ness instead of containing it (that is, waiting on any async calls synchronously). But yes, making a function async is a backward-incompatible change, just like manually changing the return type from `T` to `Future<T>`.


Ok let me phrase it differently. If you have some library function that does some IO (for example) and you want to make it as useful as possible, would you declare it async or not? If you make it blocking, then you'd ruin any opportunity to have it called from async functions (because they don't want to be blocked). Therefore, you'd make it async. So that means that all IO library routines will become async, and possibly a lot of others too. Wouldn't it make more sense to have async be the default?


But when all those IO functions are rewritten to be async, their public names can be different. The sync versions needn't change names, so none of the callers would need to change. Nor would the sync versions need to duplicate code, because they can just be thin wrappers that call and wait on their async counterparts. (Note that this wouldn't be possible in JS.)


and it can change. Let's say your bandwidth goes from 1GbE to 10GbE and now your bottleneck has changed. You may want to refactor which task is async.


Wouldn't it be desirable to replace async with sync, then?

I'd rather dirty the signatures of blocking/synchronous methods then of the rest.


Sync is still much more common than async. I'd rather write `await { httpCall }` than `blockOn { 1 + 1 }`


That would make a lot of sense, but wouldn't be very practical to add in to an existing code base.


Unlike JS, most languages, including Rust, are not async by default, so this would not make sense.


How is JS async by default?


When people say this, they usually mean "a significant amount of things JavaScript is used for are asynchronous, and IO is virtually always asynchronous," rather than something more literal. The language has asynchronicity deep in its bones, even if it's not entirely made out of it.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

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

Search: