Rust is sold as a language where the compiler catches your mistakes. That is true. The compiler also catches your intentions, your architecture and your willingness to continue. The borrow checker is not a safety feature. It is a very opinionated colleague who will not approve the pull request until you restructure the module.

The borrow checker is a conversation you cannot leave

You want two references. The compiler wants one mutable reference or any number of immutable ones, not both, not at the same time, not across the await point. You reach for Rc<RefCell<T>>, then Arc<Mutex<T>>, then a channel, then a different architecture. The bug you were trying to fix is still there, but now it has a thread-safe wrapper.

The borrow checker feedback loopborrow twiceE0499split lifetimesborrow againE0502try once moreYouCompilerRefactor
The borrow checker feedback loopThe compiler is not asking a question. It is explaining your program to you.

Diagram: The borrow checker feedback loop. You connects to Compiler (borrow twice). Compiler connects to You (E0499). You connects to Refactor (split lifetimes). Refactor connects to Compiler (borrow again). Compiler connects to You and fails (E0502). Compiler connects to Refactor asynchronously (try once more).

Async is a second language

Async Rust is not Rust with an await. It is a separate ecosystem with its own traits, its own pinning rules and its own runtime choice. Tokio, async-std, smol and embassy all disagree about the details. Function colours are real, Send bounds propagate through your entire call graph, and a future that is not Unpin will find you.

The async runtime zooruntimemaybeembeddedHTTPHTTPhardwarenot compatibleApplicationTokioasync-stdEmbassyHyperTideHAL
The async runtime zooOne language, four runtimes, and a compatibility layer for each pair.

Diagram: The async runtime zoo. Application connects to Tokio (runtime). Application connects to async-std (maybe). Application connects to Embassy (embedded). Tokio connects to Hyper (HTTP). async-std connects to Tide (HTTP). Embassy connects to HAL (hardware). Hyper connects to Tide and fails (not compatible).

unsafe is the real language

The tutorial teaches safe Rust. The crates you depend on use unsafe to implement the data structures the standard library does not expose. Soundness bugs in those blocks are not caught by the borrow checker, because the whole point of the block is to tell the borrow checker to leave. cargo-geiger exists because counting unsafe is a reasonable thing to do before shipping.

Where the safety actually liveswrapsdereferencestouchesout of scopeif wrongSafe APIunsafe BlockRaw PointerMemoryBorrow CheckerSoundness Bug
Where the safety actually livesThe safe API is a thin coat of paint over a block nobody wants to audit.

Diagram: Where the safety actually lives. Safe API connects to unsafe Block (wraps). unsafe Block connects to Raw Pointer (dereferences). Raw Pointer connects to Memory (touches). Memory connects to Borrow Checker and fails (out of scope). unsafe Block connects to Soundness Bug asynchronously (if wrong).

Compile time is a feature request

Monomorphization means generic code is compiled once per concrete type. Procedural macros mean parts of your source are generated by another program. Incremental compilation helps until it does not. A clean build of a mid-sized dependency graph is a coffee break with a progress bar.

Rust is a good language for the problems Rust is good at. The problem is that every team now believes their CRUD service is one of those problems.