Hacker News new | ask | show | jobs
by hombre_fatal 2210 days ago
This doesn't make sense to me.

Javascript is a breath of fresh air after writing async code in almost every other ecosystem from Go to Java to Python to Swift. Pretty much any async code snippet in those languages can be improved by porting it to Javascript.

Async/await + the ubiquitous Promise make it my go-to choice for writing anything networked. Especially over the other popular dynamically typed languages.

5 comments

Well, if so I don't think it was true until recent JS versions. And I'm not sure you're picking very good languages for your async comparison. (Python? yikes.) Long-standing async support in C# or newer support in Kotlin, or almost any language with real co-routines will fair better than JS. As for as promises go, using CPS with or without Promise wrappers seems pretty old hat.

More to the point I think, is that Node (and Deno, FWICT) lack general native or green thread support for true multi-processing without serialization to separate clusters, so you are forced to use async and timers for long-running or parallel work.

I'm not being unfair when I enumerate the most popular languages for comparison. When people crap on Javascript, they presumably prefer another language. And C#/Kotlin aren't exactly the top picks.

Kotlin has BYOB coroutines which are hard to work with. People don't use them. Going with the C# approach where async behavior looks sync was a bad move. I predict Kotlin's coroutines will never be a centerpiece abstraction just like how people don't really use Go's channels (people in practice just go back to Mutexes).

I mean, try it. Write the equivalent to this in Kotlin:

    // get background work started now
    const background = promise()
    
    // crawl some urls concurrently as well, just 4 at a time
    const crawl = Promise.map(urls, crawl, { concurrency: 4 })

    // while that's going on, we have some work that
    // we must get done.
    for (const task of tasks) {
      await worker(task)
    }

    // worker's done, now we can wait on 
    // the crawler and background work.
    const [a, b] = await Promise.all([
      crawl.then(processResults),
      background
    ])
CSP never caught on because after you have more than one channel as a central bus (the toy architecture), you immediately descend into channel hell. In-channels, out-channels, channels over channels. Back to using pencil and paper and scouring your code to decode the classic buffer bloat problem.

A single-threaded event loop with a central promise abstraction is a great way to write networked code.

Btw, I use Kotlin in a large JVM project and I'm stuck with the horror of https://docs.oracle.com/javase/8/docs/api/java/util/concurre.... That's more likely what you'll be doing day to day with Kotlin, not playing with its toy coroutines.

Whats wrong with something like this

  import kotlinx.coroutines.*
  import kotlinx.coroutines.channels.Channel

  fun background(): Channel<String> {
      val ch = Channel<String>()
      GlobalScope.launch {
          delay(5000)
          ch.send("OK")
      }
      return ch
  }

  fun getWebsiteData(url: String): Channel<String> {
      val ch = Channel<String>()
      GlobalScope.launch {
          delay(4000)
          ch.send("website data")
      }
      return ch
  }

  fun work(): Channel<String> {
      val ch = Channel<String>()
      GlobalScope.launch {
          delay(1000)
          ch.send("work result")
      }
      return ch
  }

  fun main() = runBlocking {
      println("START")
      val bck = background()
      val urls = listOf("www.web1.com", "www.web2.com", "www.web3.com", "www.web4.com")
      val chs = urls.map { getWebsiteData(it) }
      generateSequence { work() }.take(4).forEach {
          println("sync job done: " + it.receive())
      }
      chs.forEach {
          println("data fetched done: " + it.receive())
      }
      println("background done:" + bck.receive())
      println("END")
  }
Re:Kotlin, I'm not a JVM expert/fan, so I'll take your word for that. As for the lower-level coroutining, isn't the point that this will allow you/library-writers to abstract over these and provide the higher-level abstractions that you want to use?

Regarding "C# approach where async behavior looks sync", I don't follow you here. C# is very explict with async's returning Task<> where with Go's lack of "colored" fns, for example, you don't really know when you code is async or not.

Well the async/evented execution model, and omitting synchronize, complex "happens-before" semantics, and shared memory a la Java (which JavaScript and V8 lacks) is the entire point of node.js and libuv. I agree that it doesn't fit typical complex business logic with expectations of some level of isolation, but then node.js isn't a good fit for these kind of problems. Node.js is based on CommonJS, and there are/were alternative implementations of a CommonJS runtime, including process-per-request implementations like v8cgi/TeaJs, or implementations based on Rhino (Mozilla's venerable JavaScript engine written in Java) such as Ringo which can call into the JVM, and do multithreading. Complaining about this on node.js is complaining about your own decision to use node.js really. And multithreading isn't great either for these workloads; it was originally invented for coroutines in desktop apps.
> Complaining about this on node.js is complaining about your own decision to use node.js really

Yes - but there isn't much choice in the mainstream. Sure, I'd rather use .net core/Kestral, but if you want back-end JS/TS then Node is it, unless your org let's you experiment w/Deno or other.

I dislike javascript for much the reason you love it.

Promises just mean having to manually build and manipulate cooperatively multitasking green-threaded call-stacks. It was a dirty necessity following the inability of the earlier callback patterns to manage the level of complexity people were attempting to express in the language.

Even Rust implemented async/await. It's one of the few nice ways to write asynchronous code.

People who think that it's "manual" only haven't yet discovered the trade-offs of what they think is their preferred solution. There is no free lunch here.

It's like sniping at people who chose different prongs of the CAP theorem while thinking you're somehow in a superior position.

As someone with most of my coding experience in JS/TS, what would you say is a good alternative to explore?
Go with goroutines and channels is a nice way to handle concurrency.

At least it was the one that was easiest for me to wrap my head around and actually improved the performance of my code without weird race conditions or bugs.

Go concurrency is basically threads + the ability to choose between classic mutex synchronization (and all the problems with that like reentrancy bugs) or burn yourself in channel hell with deadlocks and buffer bloat.
Have to you agree with you there. I was pretty hyped for Go some 4-5 years ago but discovered its parallelism/concurrency model was essentially mutexes / condition variables / bounded queues when you get down to it. It's an improvement over doing it manually -- but definitely not a big one.
Erlang / Elixir.
As someone who mainly writes Scala, I really enjoy working with effect types that are also monads. Cats effect, Monix and ZIO are some examples.
Erlang.
If you think async/await is the holy grail of networking, you're up for a surprise in the next few years.

The problem with async/await: http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...

A solution proposed by none other than Java (available as an experimental feature in JDK 15): http://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part1.h...

This seems to me to obsolete async/await, quite honestly.

Don't fall into the trap of thinking that just because you've found a blog post that could enumerate the downsides of something that the alternatives don't have the same trade-offs.

I've responded to that ancient blog post many times on HN by now. Since you must think that blog post is pretty good criticism of async/await, here's a little challenge for you: which language do you think doesn't have an equivalent of the "red vs blue" problem? :)

> Don't fall into the trap of thinking that just because you've found a blog post that could enumerate the downsides of something that the alternatives don't have the same trade-offs.

I've been using async/await for a long time and I thought it was great, but since I started using JDK15 virtual Threads, I feel like it's way superior in every way. If you have some trade offs you would like to mention, please do.

> which language do you think doesn't have an equivalent of the "red vs blue" problem?

Any language that distinguishes between async and non-async functions will have this problem. I believe one language that works around that is Erlang, as Erlang seems to do everything async (but it's not visible to users, but I am not sure). The proposed Java virtual Threads solve that problem as well. You should open your mind to new ways of doing things, this is by no way a solved problem as you so strongly seem to believe.

A link to one of your responses would be spiffy.
I skimmed the Loom article and I don't see how it obsoletes async/await. It seems primarily focused on performance.

But that's not the problem that async/await solves for me. I like JS concurrency because shared-memory concurrency is very hard to program correctly. By using a single-threaded event loop with async/await, I know exactly when it's possible for the contents of memory to change out from under me: only when I `await`. This makes it much easier for me to reason about the correctness of my application.

Given that Node.js makes it easy to spin up processes on multiple cores (e.g., with a library like worker-farm), I get full CPU utilization without the safety and liveness problems that shared-memory threads have. This is very nice.

> If you think async/await is the holy grail of networking,

They wrote that it's better, not perfect.

Why do you need async for "anything networked"? Genuinely asking.

There are many ways of doing I/O, but nowadays everyone seems to do everything async without giving it a second thought.

It's more about the concurrency abstractions available. And half the joy of writing async code in JS is that it's single-threaded.

Doing many things at the same is inherent to doing networked stuff. How ergonomic that is going to be is one of the only real things that sets languages apart.

> Async/await + the ubiquitous Promise

There's a legitimate concern about async-awaits — they don't have an inbuilt cancellation mechanism. May not be a big thing for the server; but definitely a big thing for the client.

See, for example: https://twitter.com/getify/status/1171820070538022914

Free cancelation has been a pipedream since day one of computer science.

We don't want it so badly that we want to pass around poison channels or cancelation tokens. People don't even bother threading cancelation contexts in Go because it's annoying and you still have to write disposal logic for anything worth canceling (which usually isn't possible anyways -- e.g. can't undo that database query that's in flight).

There are cute things you can do with generators in the UI where cancelation cascades make a lot of sense, but notice how nobody actually cares enough to use redux-saga nor this guy's library.

> There's a legitimate concern about async-awaits — they don't have an inbuilt cancellation mechanism. May not be a big thing for the server;

They are definitively a big thing for the server.

The lack of cancellation mechanism and pre-emption + single threaded nature are the main reasons why the P95 latency of Nodejs App tend to get horrible under load.