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., -sync and -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_on in 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 unasync or 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 await is 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 asyncio is 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 async are 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.