Terrific post. I think several of the more disagreeable commenters here are engaging with it piecemeal and projecting stronger opinions onto it than it actually has. I interpreted it as more of a review/analysis/set of musings by a qualified expert in an area than a bunch of decisive hot takes.

It definitely makes me wonder if the original sin of Rust and many of the other colored-function-based concurrency systems was limiting the enforcement of color-can-only-call-color to only async functions. Would a language that disallows calls back from async functions into blocking functions make sense? Probably not. But what if we split "blocking" into two categories: functions that provably blocked for constant time (or even constant-less-than-X time; simple systems like BPF can verify that already, so I'd assume it's possible to get more nuanced here with effort) versus functions that did not (including I/O, while-true-with-non-statically-verifiable-condition)? If we disallowed calls from async functions into functions of the latter category but not the former, would that be ergonomic/accessible for programmers?

(A minor quibble/criticism of an otherwise great post: I think the discussion of "blocking functions" should get a crisp definition up front, qualifying that "blocking" in this case means undesirably long or potentially indeterminate blocking of executor threads, so readers don't get confused and wonder whether the implication is that any synchronous function call is inherently--rather than just potentially--toxic to good async behavior).

Continuing to think about the idea of "async code can't call 'bad blocking' code": I suspect that one of the reasons this isn't widespread is that it really does require compiler/runtime validation that code down a call stack is good/bad. Erlang almost got there, but (just like Boats points out happened with Rust) the C FFI chimera stopped them in the home stretch and they settled for asking users to tag dirty vs. clean NIFs. But maybe compiler awareness of call path blocking risk is advanced enough today that such a system could be implemented? It definitely wouldn't be simple (...he said, as if he were anything but a rank amateur when it came to understanding compilers). It definitely would solve the mutex-held-across-await-point class of problems.

Two other points that stood out to me that I thought were worth mentioning:

1. The rejoinder to the blocking-reqwest-vs-ureq situation is well put. Honestly, Boats could have gone further here: I think that a large proportion of the complaints/reluctance around using synchronous APIs that wrap block_on or whatever are fundamentally aesthetic/emotional objections arising from people's bad vibes around including or separately instantiating an async runtime inside a library just to provide a blocking API. In practice, the compile overhead is rarely severe and nearly always paid extremely infrequently, and the runtime overhead in time/memory is negligible in release builds. Supporting the emotional resistance here is, I think, a lot of user ignorance around the practical differences between, say, how a current_thread Tokio entry point runs Rust futures and how a libuv loop or a boost::asio context or whatnot run futures in JS/C++/etc. While Rust and Tokio aren't magic (and while I would love more crates to be async-to-syncable with pollster alone), I think that folks often project assumptions about heaviness/performance from other systems onto parts of the Rust ecosystem where they don't hold as true.

2. The Golang callout is fair (and was hopefully cathartic to write), but in addition to being punchy I think there's a lesson to be learned there, albeit a bitter one: Go is widely considered to have wildly succeeded at/"solved" the ergonomics of concurrent colored-function programming for a bunch of technical reasons that Boats calls out ("this is for network servers, C interop's not priority 1", growable stacks, GC, nonzero cost scheduler, etc.), but I think it's also important to note that the problem was addressed to completion in the initial version of the language. Rust's async functionality was not; it was added later. And while it was added carefully, I think that the fact that it was not part of the first coat of paint inevitably increases the severity of some of its pain points, technical constraints aside. I think the lesson that features in the first draft are often the most beloved/powerful holds true in other ways here: consider the trait system and borrow checker, two of the most important features of Rust that were present in v0 (or at least very early on? I don't honestly know) compared to Go's slow and acrimonious adoption of generics in 1.18. I guess another way of putting the lesson is "get it right the first time", alternatively phrased as "hindsight is 20/20". Or maybe I'm extrapolating patterns from unrelated technical phenomena, who knows?