Ownership Without the Annotations: What Application-Level Rust Looks Like
Source: lobsters
The reputation Rust has accumulated over its decade in the public eye is partly deserved and partly a product of the wrong kind of examples. Tutorial code, library internals, and systems work show Rust at its most demanding: explicit lifetime annotations threading through every function signature, unsafe blocks for FFI, custom allocators, pinned futures. That is real Rust, but it is not the only Rust.
The case for high-level Rust argues that you can get most of Rust’s guarantees while sidestepping most of its ceremony. That is true, and worth spelling out in detail, because the specific patterns involved are less obvious than they first appear.
Library Rust vs Application Rust
The most important distinction almost nobody makes explicit: library code and application code face different constraints.
A library crate on crates.io needs to be maximally flexible. It cannot own its data if callers might want to pass borrowed references. It cannot use String where &str would do, because that forces an allocation on callers who already have their string. It needs lifetime parameters so users can store references in structs. The result is code that looks like this:
pub fn extract<'a, T>(
source: &'a str,
parser: impl Fn(&str) -> Option<T>,
) -> Option<(&'a str, T)> {
// ...
}
That is not complexity for its own sake. It is real generality with real tradeoffs, and library authors are right to write it that way.
Application code, your binary, does not have those constraints. You control every call site. You choose the types. Nobody is going to wrap your binary in a crate and complain that you used String instead of &str. So you can write:
fn extract(source: String, pattern: &str) -> Option<String> {
source.find(pattern).map(|i| source[i..].to_string())
}
Yes, that clones. No, it does not matter for most programs.
The Specific Patterns
Three patterns cover the majority of situations where people fight the borrow checker.
Clone instead of threading lifetimes. When ownership gets complicated, clone the data. For small strings and typical structs, cloning costs nanoseconds. For a web server handling hundreds of requests per second, this is not a bottleneck. The compiler still guarantees no use-after-free, no dangling pointers, no data races. You pay in allocations, not in safety.
// fighting ownership
fn greet<'a>(name: &'a str) -> String {
format!("Hello, {name}")
}
// owning everything, no lifetime juggling
fn greet(name: String) -> String {
format!("Hello, {name}")
}
The first form is not wrong; it is simply unnecessary when you control the call site and can pass an owned String directly. The second form compiles with zero annotations and the semantics are clear.
Arc<Mutex<T>> for shared mutable state. Raw concurrency in Rust requires careful thought about ownership across threads. Arc<Mutex<T>> wraps that into a runtime-checked pattern that the compiler still enforces at the type level:
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
let cache: Arc<Mutex<HashMap<String, String>>> =
Arc::new(Mutex::new(HashMap::new()));
let cache_clone = Arc::clone(&cache);
tokio::spawn(async move {
let mut map = cache_clone.lock().unwrap();
map.insert("key".into(), "value".into());
});
The atomic reference counting adds roughly 10 to 15 nanoseconds per clone operation. The mutex adds overhead under contention. For anything outside of high-frequency numerical workloads, this is acceptable, and you get a data-race-free guarantee that most other languages cannot provide at all.
anyhow for error handling. Rust’s Result<T, E> type is the right model for error propagation, but defining custom error enums for every function is tedious when you just want errors to bubble up with context. The anyhow crate lets you treat errors as opaque values with human-readable context attached:
use anyhow::{Context, Result};
async fn fetch_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 = toml::from_str(&content)
.context("config file contains invalid TOML")?;
Ok(config)
}
The ? operator propagates errors upward automatically. with_context attaches descriptions that show up in the error chain. You get better error messages than most Python programs produce, with no boilerplate enum definition, no impl Display, no impl Error.
What the 2018 Edition Changed
A significant amount of Rust pain people describe comes from blog posts and tutorials written before 2018. The 2018 edition introduced Non-Lexical Lifetimes (NLL), which fundamentally changed how the borrow checker operates.
Before NLL, borrows lasted until the end of the lexical scope they appeared in, which produced spurious errors in patterns that were obviously safe to a human reader. NLL replaced lexical scoping with actual dataflow analysis. The compiler now tracks where borrows are genuinely used rather than where the enclosing brace is. The set of programs that compile without explicit lifetime annotations expanded substantially as a result.
The Polonius project, a more precise borrow checker based on Datalog, has been available behind a nightly flag for several years and will eventually stabilize. It handles patterns that NLL still rejects, particularly around mutable borrows returned from match arms and reborrowing across function calls. Some code that requires lifetime annotations today will eventually just compile without them.
Most people learning Rust from resources older than 2019 are learning a harder version of the language than exists today. The 2021 edition further tightened closure capture semantics and improved iterator ergonomics. The language keeps getting more usable at the margins.
The Performance You Keep
The reason high-level Rust is worth the effort rather than a pointless middle ground is that the performance floor stays high even when you take the easy paths.
Clone-heavy application Rust typically runs between two and ten times faster than equivalent Python, depending on the workload, without touching unsafe, without manual memory management, without lifetime gymnastics. For most backend services, CLI tools, and automation scripts, that is more than adequate.
The type system and ownership model still prevent entire categories of bugs at zero runtime cost: no null pointer exceptions, no use-after-free, no iterator invalidation, no data races without explicit unsafe code. These guarantees do not erode when you start using String instead of &str or cloning values instead of borrowing them.
The tooling remains excellent regardless of which style you write. cargo handles dependency management more reliably than npm or pip. rust-analyzer provides accurate, fast completion and diagnostics. clippy catches a wide range of common mistakes and anti-patterns. None of this requires writing idiomatic low-level systems code. It applies to all of it.
The Pain That Stays
The 20% is real and should not be minimized.
Async Rust is still rough at the edges. The stabilization of async functions in traits in Rust 1.75 was a significant improvement, but async code still requires understanding Send bounds, executor compatibility between crates, and the occasional Pin<Box<dyn Future + Send>> when dynamic dispatch enters the picture. No amount of strategic cloning makes this disappear; it is a fundamental complexity of the async model.
Compilation times are slow. A medium-sized Rust project takes 30 to 90 seconds for a clean build. Incremental compilation helps substantially in practice, but the feedback loop is noticeably longer than Go or TypeScript. This is a fixed cost that high-level code does not reduce.
The learning curve is front-loaded. Even with the techniques above, you need to understand ownership conceptually before you can write Rust that compiles. The borrow checker will reject patterns that work in any garbage-collected language, and even good error messages require some foundation to interpret. There is no shortcut past this initial investment, only strategies for limiting how often you encounter the harder edges afterward.
Where This Leaves You
If you have been avoiding Rust because of lifetime complexity, the high-level approach is a genuine on-ramp. The ecosystem has matured around it: tokio for async runtime, serde for serialization, reqwest for HTTP, sqlx for typed database queries. These crates are designed with ergonomics as a first-order concern, and they compose well.
For tools and services I want to be reliable, I reach for Rust when a garbage-collected language would add GC pauses I cannot afford or when I want a single statically linked binary I can deploy anywhere. A command-line tool that reads files, calls APIs, and writes output is entirely writable in high-level Rust in a day, without a single explicit lifetime annotation, using the patterns above. The resulting binary will not segfault, will not have memory leaks, and will handle errors explicitly at every step.
The borrow checker is not the enemy of productive Rust. It is the mechanism that makes all the other guarantees possible. High-level Rust just lets you deal with it mostly indirectly, at the cost of some allocations and some runtime checking. For a wide range of programs, that tradeoff is clearly correct.