Comment by rowanG077
The case I'm making is not that different Io context are good. The point I'm making is that mixing them is almost never what is needed. I have seen valid cases that do it, but it's not in the "used all the time" path. So I'm more then happy with the better ergonomics of traditional async await in the style of Rust , that sacrifices super easy runtime switching. Because the former is used thousands of times more.
If I'm understanding correctly (that most code and/or most code you personally write doesn't need that flexibility) then that's a valid use case.
In practice it should just be a po-tay-to/po-tah-to scenario, swapping around a few symbols and keywords vs calls to functions with names similar to those keywords. If that's all you're doing then passing around something like IO (or, depending on your app, just storing one once globally and not bothering to adhere to the convention of passing it around) is not actually more ergonomic than the alternative. It's not worse (give or take a bunch of bike-shedding on a few characters here and there), but it's not better either.
Things get more intriguing when you consider that most nontrivial projects have _something_ interesting going on. As soon as your language/framework/runtime/etc makes one-way-door assumptions about your use case, you're definitionally unable to handle those interesting things within the confines of the walls you've built.
Maybe .NET Framework has an unavoidable memory leak under certain usage patterns forcing you to completely circumvent their dependency-injection code in your app. Maybe your GraphQL library has constrained socket assumptions forcing you to re-write a thousand lines of entrypoint code into the library (or, worse, re-write the entire library). Maybe the stdlib doesn't have enough flexibility to accomodate your atypical IO use-case.
In any one app you're perhaps not incredibly likely to see that with IO in particular (an off-the-cuff guesstimate says that for apps needing _something_ interesting you'll need IO to be more flexible 30% of the time). However, when working in a language/framework/runtime/etc which makes one-way-door assumptions frequently, you _are_ very likely to find yourself having to hack around deficiencies of some form. Making IO more robust is just one of many choices enabling people to write the software they want to write. When asking why an argument-based IO is more ergonomic, it's precisely because it satisfies those sorts of use cases. If you literally never need them (even transitively) then maybe actually you don't care, but a lot of people do still want that, and even more people want a language which "just works" in any scenario they might find themselves in, including when handling those sorts of issues.
=== Rust async rant starts here ===
You also called out Rust's async/await as having good ergonomics as a contrast against TFA, and ... I think it's worth making this comment much longer to talk about that?
(1) Suppose your goal is to write a vanilla application doing IO stuff. You're forced to use Tokio and learn more than you want about the impact of static lifetimes and other Rust shenanigans, else you're forced to ignore most of the ecosystem (function coloring, yada yada). Those are workable constraints, but they're not exactly a paragon of a good developer experience. You're either forced to learn stuff you don't care about, or you're forced to write stuff you don't think you should have to write. The lack of composability of async Rust as it's usually practiced is common knowledge and one of the most popularly talked about pain points of the language.
(2) Suppose your goal is to write a vanilla _async_ application doing IO stuff. At least now something like Tokio makes sense in your vision, but it's still not exactly easy. The particular implementation of async used by Tokio forces a litany of undesirable traits and lifetime issues into your application code. That code is hard to write. Moreover, the issues aren't really Rust-specific. Rust surfaces those issues early in the development cycle, but the problem is that Tokio has a lot of assumptions about your code which must be satisfied for it to work correctly, and equivalent libraries (and ecosystem problems) in other langugages will make those same assumptions and require the same kinds of code modifications from you, the end user. Contrasted with, e.g., Python's model of single-threaded async "just working" (or C#'s or something if you prefer multi-threaded stuff and ignore the syntactic sharp edges), a Tokio-style development process is brutally difficult and arguably not worth the squeeze if you also don't have the flexbility to do the async things your application actually demands. Just write golang greenthreads and move on with your life.
(3) Suppose your goal is something more complicated. You're totally fucked. That capability isn't exposed to you (it's exposed a little, but you have to write every fucking thing yourself, removing one of the major appeals of choosing a popular language).
I get that Zig is verbose and doesn't appeal to everyone, and I really don't want to turn this into Rust vs Zig, but Rust's async is one of the worst parts of the language and one of the worst async implementations I've ever seen anywhere. I don't have a lot of comment on TFA's implementation (seems reasonable, but I might change my mind after I try using it for awhile), but I'm shocked reading that Rust has a good async model. What am I missing?