The bane of my existence: Supporting both async and sync code in Rust
Supporting both synchronous and asynchronous code in Rust exposes a deeper tension in modern programming: once a library adopts async I/O, its “color” tends to spread through the API, complicating usage for callers who want simple, blocking interfaces. Commenters contrast Rust’s ecosystem—where multiple runtimes, lack of standardized async traits, and feature-flag constraints make dual sync/async support painful—with languages like Go, JavaScript, Python, and Zig, each of which bakes in different trade-offs between ergonomics, performance, and runtime complexity. Proposed workarounds range from separate sync/async crates and `block_on` wrappers to sans-I/O designs and effect systems, but none fully eliminates the maintenance and design burden for library authors.
Rust library pain: supporting sync & async
- Core issue: libraries want to offer both sync and async APIs without code duplication, feature-flag explosions, or forcing an async runtime on sync users.
- Cargo feature rules (“features must be additive”) make mutually exclusive sync/async configurations awkward.
- Maintaining two crates (e.g.,
-syncand-async) or two parallel code paths was tried and found cumbersome. - Users also dislike being forced into async because a dependency chose it.
Proposed approaches
- Use
block_onin a dedicated thread to expose a sync façade over an async core; many consider this a pragmatic compromise. - Keep only an async API and suggest that sync users block on it, though this still “colors” the caller.
- Separate crates or modules for sync and async, with some bikeshedding over naming conventions.
- Sans-IO style: expose pure request/response types and let callers choose sync or async HTTP clients.
- Macros / codegen (as in Python’s
unasyncor Nim’s “multisync”-like ideas) to auto-generate sync and async variants.
Async vs sync: conceptual debate
- Some see async as “beautiful” and the most natural way to express complex state machines and high-concurrency I/O.
- Others find futures and state machines less natural than scoped threads for many workloads.
- Several argue that async’s benefits appear only when many tasks run concurrently; for purely sequential code it adds mental overhead.
- There is disagreement on whether async avoids concurrency bugs; others point out race conditions can still arise when
awaitis inserted into previously straight-line code.
Cross-language comparisons
- JS: async integrates smoothly because I/O was always callback-based and single-threaded; hiding async under sync is still hard.
- Python: async opens new patterns but
asynciois criticized as heavy and sometimes slower than simple threads. - Go: praised for a single “green” concurrency model; debate over whether this truly avoids function coloring or just standardizes one “color.”
- Haskell: green threads and
asyncare seen as ergonomically nicer; immutability helps reasoning about concurrency.
Function coloring and runtimes
- “Function coloring” is widely cited as the underlying pain: async annotations propagate virally up call stacks and across modules.
- Rust’s lack of a built-in runtime and fragmentation across Tokio/other runtimes exacerbate friction (async traits, runtime coupling, cancellation semantics).
- Some hope for future solutions (keyword generics, effect systems, async-generic designs) to reduce duplication and make code runtime-agnostic.
Immutability, concurrency, and bugs
- One subthread argues immutability pairs well with async/threads by eliminating data races; others counter that immutability can be overhyped and shifts complexity to update mechanisms.
- There is no consensus; participants provide anecdotes both for and against immutability as a “silver bullet” for concurrent correctness.