There are two ways to write Rust, and conflating them is responsible for most of the language’s reputation for difficulty.
The first is library Rust: code on crates.io that must be maximally flexible, zero-copy where possible, and lifetime-annotated wherever references cross struct boundaries. The second is application Rust: a binary you ship that nobody wraps in another crate, where owned types are free to use everywhere and the compiler still prevents every category of memory safety bug that matters.
Most tutorials, and most of the “Rust is painful” discourse, describe the first kind. A recent article at hamy.xyz lays out the alternative directly: embrace owned types, use Arc<Mutex<T>> for shared state, reach for anyhow for errors, and stop fighting the borrow checker on problems that don’t need that fight. The framing is 80% of the benefits with 20% of the pain. That ratio is roughly right, but the interesting question is which 80% you’re getting and whether it matters.
What the Patterns Actually Are
The core of the approach is straightforward. Use String instead of &str in struct fields and return types. Use Vec<T> instead of &[T], PathBuf instead of &Path. Lifetime annotations appear when a struct holds a reference and must describe how long that reference lives; owned types carry no such requirement:
// Every struct that stores this must carry the lifetime annotation:
struct Config<'a> {
base_url: &'a str,
token: &'a str,
}
// No annotation, goes anywhere:
struct Config {
base_url: String,
token: String,
}
For shared mutable state across async tasks, Arc<Mutex<T>> from Tokio is the standard pattern. The critical detail is that lock guards must not be held across await points. Guards are !Send, and tokio::spawn requires Send + 'static futures, so holding one across an await is a compile error rather than a runtime deadlock:
async fn update_cache(cache: Arc<Mutex<HashMap<String, String>>>, key: String, value: String) {
// Drop the guard before any await point by scoping it
{
let mut guard = cache.lock().await;
guard.insert(key, value);
} // guard drops here
notify_subscribers().await; // safe: no lock held
}
For error handling, anyhow replaces custom error enums in application code. The ? operator propagates errors up the call stack, .context() adds human-readable context at each level, and the final error message reads like a diagnostic rather than a type name. The community convention is anyhow in binaries and thiserror in libraries; they compose cleanly since libraries return typed errors that applications wrap with context.
Async Rust Enforces This Anyway
There is a deeper reason why high-level patterns are not merely ergonomic: tokio::spawn requires 'static futures. The signature is unambiguous:
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
You cannot move a borrowed local reference into a spawned task. The only options are to move owned data or wrap shared state in Arc<T>. For most async application code, this means liberal cloning and Arc-wrapped state are not a concession to convenience but a structural requirement of the runtime model.
Poise and Serenity, the two dominant Discord bot frameworks, are designed around this. BotState in a Poise application is cloned on every command invocation; making all heavy fields Arc-wrapped means the clone is cheap:
#[derive(Clone)]
struct BotState {
db: Arc<sqlx::PgPool>,
cache: Arc<RwLock<HashMap<u64, CachedUser>>>,
config: Arc<BotConfig>,
}
The one remaining lifetime annotation in a typical Poise application ('a in the Context type alias) comes from the framework’s API design, not from anything the developer needs to reason about in their own types.
What You Still Get
The important question is what safety properties survive the liberal use of .clone() and Arc. The answer is most of the ones that matter for application code.
Null pointer dereferences are impossible. Option<T> forces handling the None case at compile time; there is no nil. Data races are prevented at compile time via the Send and Sync marker traits, not detected at runtime by a race detector run under specific test conditions. Mutex<T> structurally owns its data, so the only way to access it is through .lock(). There is no advisory convention to violate. Use-after-free and double-free are still compile-time impossibilities, since the borrow checker tracks ownership even for owned values. Exhaustive pattern matching on enums means adding a new variant to an event type produces compiler errors at every match that doesn’t handle it. Errors propagated via Result<T, E> with ? cannot be silently discarded without explicit acknowledgment.
Microsoft and Google have both reported that memory safety bugs account for roughly 70% of serious CVEs in their codebases. High-level Rust closes that category just as firmly as zero-copy Rust. The performance headroom you’re leaving on the table has no bearing on whether a bounds check runs or whether a lock is held.
The cost of a String::clone() on a typical Discord message is under 50 nanoseconds on modern hardware. A round-trip to Discord’s API takes 50 to 200 milliseconds. The clone is not the bottleneck and will never be found by a profiler in this context.
The Go Comparison Is About Type Systems, Not Runtimes
The natural comparison for high-level Rust is Go, and the framing matters. At the runtime level, both languages use heap allocations, have low-overhead concurrency, and abstract over manual memory management. For I/O-bound services processing network requests against a database, the throughput difference between Go and clone-heavy Rust is real but not decisive for most applications.
The divergence is at the type level, and it is substantial. Go’s sync.Mutex is advisory: nothing in the language prevents accessing guarded data without holding the lock. Rust’s Mutex<T> owns its data structurally; the type system enforces the invariant. Go nil pointers crash at runtime; Rust’s Option<T> enforces handling at compile time. Go’s interface-based polymorphism requires type assertions that can panic; Rust’s enum pattern matching is exhaustive and checked at compile time. Go’s goroutine safety relies on the race detector, which runs only during explicitly instrumented test runs. Rust’s Send and Sync bounds prevent races before the binary is produced.
The 2025 State of Rust Survey shows the borrow checker is still a barrier for newcomers but has become background noise for experienced developers. The dominant pain points now are compile times and async edge cases, both of which affect experienced developers regardless of how idiomatic their ownership patterns are.
What Remains Hard
Compile times are structural and will not improve with high-level style. A medium Rust project with a typical dependency tree takes 30 to 90 seconds for a clean build. Go builds the same project in under a second. Mitigations exist: the mold linker reduces link time by 2 to 5 times on Linux, sccache helps on CI with warm caches, and the Cranelift backend cuts debug build times by 30 to 50 percent. None of these solve the problem; they reduce it.
Async edge cases persist. async Drop is still not stable. Lock guards acquired in one async block and accidentally held across an await will be caught by the compiler, but the error messages for Send bound violations can be dense and require some practice to read efficiently. The stabilization of async fn in traits in Rust 1.75 (December 2023) and async closures in Rust 1.85 (February 2025) have resolved long-standing friction points, but the async story is still not as clean as the synchronous one.
For zero-copy parsing or hot buffer processing, owned types genuinely are the wrong tool. Crates like winnow and bytes exist for those domains, but they require the borrow-checker reasoning that high-level style explicitly sets aside.
The Language Has Changed
Developers learning from tutorials written before 2019 are learning a harder version of the language than currently exists. Non-Lexical Lifetimes, introduced in the 2018 Edition, replaced lexical borrow scope rules with dataflow analysis, eliminating an entire category of spurious borrow checker errors. The Polonius project on nightly will eventually remove more. The language keeps narrowing the gap between what developers write and what the compiler accepts without complaint.
High-level Rust is not a workaround or a learning crutch to be discarded once you understand ownership. For application code, it is the appropriate tool. The borrow checker’s value is not in forcing you to avoid heap allocation; it is in making a class of bugs structurally impossible. Those guarantees hold whether you are writing zero-copy parsers or Discord bot command handlers that clone freely. The question is whether the application’s performance requirements justify the additional complexity, and for most applications, they do not.