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.
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.
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.
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")
}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.
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.
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.
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.
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.
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.
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? :)
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.
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.
They wrote that it's better, not perfect.
There are many ways of doing I/O, but nowadays everyone seems to do everything async without giving it a second thought.
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.
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
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.
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.
https://medium.com/flutter-community/the-ultimate-javascript...
I have worked extensively with Typescript, which I consider to be the minimum viable solution within the Node ecosystem. Typescript shouldn't even be seen as a 'nice-to-have. It should be seen as a required remedy for some of the mistakes of a terribly engineered language. Javascript should never have been considered a serious language for back-end development. Node.js to me seems like someone's 'Frankenstein's monster'-esque experiment that escaped its captivity and wreaked wide-scale havoc on the surrounding world.