There is a version of Rust that looks nothing like the language described in most tutorials. No lifetime annotations, no &'a str, no fighting the borrow checker over who owns what. Just owned types, liberal cloning, Arc<Mutex<T>> everywhere shared state is needed, and anyhow swallowing every error. It feels almost like writing Python with a type checker. And for a large class of programs, it works extremely well.
This is what a recent piece on hamy.xyz calls “high-level Rust” — the idea that you can write productive, safe Rust without internalizing the harder parts of the ownership model. The framing is useful. But it undersells both what you get and what you actually give up.
The Pattern Set
The practical core of high-level Rust is a handful of habits that eliminate most lifetime friction:
Use String, Vec, and HashMap everywhere. Slice types (&str, &[T]) force you to think about where data lives and how long references are valid. Owned types just work. Your structs hold String fields, your functions accept String arguments (or impl Into<String> if you’re feeling generous), and the compiler never asks you to annotate how long anything lives.
// Low-level: lifetime annotations required
struct Config<'a> {
name: &'a str,
value: &'a str,
}
// High-level: just own the data
#[derive(Debug, Clone)]
struct Config {
name: String,
value: String,
}
The cost is a heap allocation per string instead of a borrow. In a CLI tool or a web server, this is irrelevant.
Use Arc<Mutex<T>> or Arc<RwLock<T>> for shared state. The borrow checker’s hardest problems come from sharing mutable state across scopes or threads. Arc<Mutex<T>> sidesteps the static analysis entirely and enforces safety at runtime instead. You pay a small overhead from atomic reference counting and lock acquisition, but you get shared mutable state that the compiler guarantees can’t cause data races.
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct AppState {
db: Arc<Mutex<HashMap<String, String>>>,
}
async fn handler(state: AppState, key: String) -> Option<String> {
let db = state.db.lock().unwrap();
db.get(&key).cloned()
}
This is the pattern almost every Axum or Actix-web tutorial uses for shared state. It is idiomatic in application code, not a hack.
Use anyhow for error handling. Defining your own error enum with thiserror and implementing Display and From for every variant is the “correct” approach for libraries. For applications, anyhow gives you anyhow::Result<T> and the ? operator, and you never think about error types again. Any error gets wrapped, propagated, and printed with a backtrace if you want one.
use anyhow::{Context, Result};
async fn load_config(path: &str) -> Result<Config> {
let content = tokio::fs::read_to_string(path)
.await
.with_context(|| format!("Failed to read config from {path}"))?;
let config: Config = serde_json::from_str(&content)
.context("Config file is not valid JSON")?;
Ok(config)
}
Clone liberally. When moving a value into a closure or passing it to a spawned task, cloning is almost always the right call in application code. The borrow checker’s complaints about moved values are often a signal that you should .clone() rather than restructure your program. A clone of a String is a microsecond. A clone of an Arc is an atomic increment.
What You Get for Free
Even written this way, Rust gives you things that are genuinely hard to get elsewhere.
Memory safety without a garbage collector. No use-after-free, no double-free, no dangling pointers. RAII means resources are cleaned up when they go out of scope. You cannot accidentally leak memory through reference cycles (without Rc::cycle, which requires explicit effort). This is the property that makes Rust compelling for systems code, and it does not require fighting lifetimes to get it.
No data races. Arc<Mutex<T>> is slower than the zero-cost version using lifetimes and references, but it still statically prevents you from accessing shared state without holding the lock. The compiler refuses to compile code that sends a non-Send type across threads. In Go, data races are a runtime concern; in Rust, most of them are a compile-time error.
Exhaustive pattern matching. Rust’s match statement forces you to handle every variant of an enum. Combined with Option and Result, this eliminates entire categories of null pointer exceptions and unhandled errors that plague other languages. You get this regardless of how you write your ownership code.
The tooling. cargo is one of the best build systems and package managers in existence. clippy catches real bugs. rustfmt keeps code consistent. cargo test is built in. You get all of this whether you’re writing zero-copy parsers or liberally cloning strings.
How It Compares to Go
Go is the language people most often choose as an alternative when Rust feels too complex. At the high-level Rust style, the productivity gap between the two narrows considerably.
Both languages give you:
- Compiled binaries with no runtime dependency
- Good concurrency primitives
- A fast feedback loop from the compiler
- Strong ecosystems for web services and CLIs
But Rust’s type system is substantially more expressive. Algebraic data types with enum let you model domain problems that Go’s interface-based polymorphism handles awkwardly. The trait system gives you zero-cost static dispatch where Go uses runtime interface dispatch. Rust’s ownership model, even at the high-level “just use Arc” style, prevents a class of bugs that Go’s race detector can only catch after the fact.
The ergonomic comparison is closer than it used to be. Rust’s impl Trait, the ? operator, and async/await (stabilized in Rust 1.39) removed a lot of the ceremony that made early Rust feel hostile. Writing a Tokio-based HTTP service in high-level Rust looks remarkably like writing the same service in Go, except you have enums instead of interface{} and the compiler catches more things.
Where the Approach Works and Where It Doesn’t
High-level Rust is appropriate for: command-line tools, Discord bots and other chatbot backends, web APIs, developer tooling, anything where the performance bottleneck is I/O rather than CPU. In these cases, the extra allocations from owned types and the overhead of Arc<Mutex> are swamped by network or disk latency. You get safe, correct, fast-enough code with a type system that catches logical errors, and you can ship it.
It is not the right approach for: embedded systems with no allocator, game engines where frame budget is tight, high-performance networking code where per-connection overhead matters, or library crates that will be used by others who do need zero-copy semantics. In these contexts, the full ownership model pays for itself, and the performance and API contracts you lose by defaulting to owned types become real costs.
The honest answer is that most programs people write professionally fall into the first category. Very few production systems are CPU-bound in the way that justifies zero-copy string handling throughout.
The Remaining Friction
The one thing the “80% of benefits, 20% of pain” framing doesn’t fully reckon with is compile times. Rust’s incremental compilation is improving steadily, and tooling like sccache and the linker replacement mold help significantly. But a medium-sized Rust project still has longer compile times than equivalent Go or TypeScript projects. This affects iteration speed in development in a way that has nothing to do with lifetimes.
The other remaining friction is the mental model. Even when you’re not writing lifetime annotations, you are still thinking in terms of ownership. When you call .clone() you are acknowledging that two owners need the same data. When you reach for Arc<Mutex> you are acknowledging that multiple threads need to mutate something. This is not the same overhead as annotating lifetimes, but it is a different way of thinking than Python or JavaScript encourage. Most developers adapt to it within a few weeks of real use, but it is a genuine adjustment.
The Practical Takeaway
The high-level Rust style is not a workaround or a crutch. It is a legitimate way to write application software in Rust that takes advantage of its safety guarantees and type system without committing to mastering its lower-level features first. The patterns — owned types, Arc<Mutex>, anyhow, .clone() at call sites — are used in production codebases at scale, including large parts of Cloudflare’s Rust infrastructure and many AWS internal services.
If you have been postponing learning Rust because the borrow checker looked impenetrable, this is the permission structure you were looking for. Start by writing Rust the way you would write Go. Use String everywhere. Clone when you’re unsure. Wrap shared state in Arc<Mutex>. The borrow checker will be largely quiet, the type system will catch your mistakes, and nothing will segfault. You can learn lifetime annotations later, for the specific cases where they earn their complexity back.