“Rust is hard” is not a useful sentence, because Rust is hard in different places depending on where you are standing. A C++ veteran and a Python veteran will both struggle, and they will struggle at almost disjoint points in the curriculum, and each will be baffled by what the other found difficult.
So: find yourself below. Naming the wall you are about to hit is worth a surprising amount — it converts “I do not understand this language” into “I am at the known hard part, which lasts about a week.”
The three big walls, in order
Every learner hits these. The curriculum is ordered to soften each one, and it is worth knowing the shape of the road before you walk it.
Wall 1 — Ownership, around week one. E0382: borrow of moved value. The cause is not that ownership is complicated; it is that most people meet moves and borrows in the same afternoon and permanently conflate them. This course separates them by an entire track: Track 1 uses only Copy types so that “Rust behaves like C ints” is a correct working model; Track 2 breaks that model with String and Vec and introduces no & at all; Track 3 is a separate track for borrowing. .clone() is explicitly permitted in Track 2 and explicitly withdrawn in Track 3 — a named “training wheels off” moment rather than a cultural sneer.
Wall 2 — Lifetimes, around month two. E0106, E0597, E0515. The cause is almost always that people are taught to write 'a before being taught why they usually do not have to. Tracks 4 through 7 deliberately sit between borrowing and lifetimes so that you accumulate genuine reasons to need them — returning a &str from a parser, holding a view into a buffer — before you meet the syntax. Then Track 8 teaches elision first: two dozen reference-returning functions with no annotations at all, and the three rules that explain them, before a single case where elision fails.
The most common misconception gets its own article: a lifetime annotation does not make data live longer. It describes a relationship that already exists. 'a is a constraint you state, not a lifetime you create.
Wall 3 — Async, month four and beyond. Pin, Waker, and the feeling that #[tokio::main] is an incantation. The cause is meeting a runtime before meeting the machine. Here, threads (Track 14) come before async (Track 16) so that “concurrency” and “async” do not fuse into one word — async is a scheduling mechanism for concurrency, not a synonym for it. And because this harness has no crates at all, there is no tokio to hide behind: you implement Future as an ordinary trait, drive it with Waker::noop(), build a real ready-queue executor out of std::task::Wake and Arc, and only then meet Pin — motivated by a self-referential future you personally constructed and watched break. The no-crates constraint is the pedagogy.
Two smaller stalls deserve names too. The Rc<RefCell<T>> cliff in Track 11: survivors of Wall 2 discover interior mutability, apply it everywhere, and start getting runtime BorrowMutError panics — which feels like a betrayal of “if it compiles, it works”. And the -D warnings stall in Track 0, which is why Track 0 is literally about the gate.
If you are coming from C or C++
You have the biggest head start and one specific trap.
What transfers: the memory model, stack versus heap, why a pointer might dangle, why copying a large struct costs something, RAII, move semantics as a concept, monomorphised generics, zero-cost abstraction as a design goal. When Rust says a value is dropped at the end of a scope, you already know what that means and why it matters.
Where you stall: the borrow checker is not doing what you think it is doing. C++ move semantics leaves a moved-from object valid but unspecified; Rust makes it statically inaccessible, which is a different thing and a stricter one. You will write code that is provably fine — you can see the lifetime with your own eyes — and be rejected. The correct response is not to fight it but to learn what shapes the checker can prove, which is a smaller set than “programs that do not crash” and a much larger set than it first appears.
The other stall is trait coherence. Rust’s orphan rule says you may implement a trait for a type only if you own the trait or you own the type. No impl Display for Vec<T> in your crate. This is what makes Rust’s generics compile-time-checked and dispatch unambiguous, and it is a real constraint you will design around with newtypes. C++ has no equivalent and no analogue.
A small mercy: there is no header/implementation split, no include order, no ODR, no forward declarations, and no build system to configure. Modules are language-level.
If you are coming from Java, C#, Go or another GC language
What transfers: static types, generics, interfaces mapping roughly to traits, the idea that a compiler catches your mistakes, and — from Go — that “share memory by communicating” is a real strategy. Rust’s Result will feel like Go’s (value, err) with better ergonomics.
Where you stall: ownership itself, all of it, at once. In a GC language nobody has ever asked you “who owns this?”, because the runtime does. Suddenly the answer must be written into every signature. The specific symptom is that your first Rust programs will be full of .clone() — you will reach for it whenever the compiler complains, it will work, and you will feel vaguely dirty.
That is fine for about two weeks. Track 2 permits it explicitly. Track 3 withdraws it, with tools that make withdrawal enforceable rather than aspirational: fixed signatures you may not edit, non-Clone payload types where cloning is a compile error, and allocation budgets that catch clones no lint can see.
The second stall is that there is no null. Option<T> is a different type from T, and the compiler will not let you use one where the other belongs. This feels like friction for a fortnight and then feels like a missing limb in every other language you use.
Go programmers get one extra surprise: Rust’s ? composes error propagation into an expression rather than four lines of if err != nil, and Rust’s enums carry data, so an error is a value with structure rather than a string.
If you are coming from Python, Ruby or JavaScript
You will hit a wall much earlier than the others, and it is not ownership.
Where you stall, first: E0308: mismatched types, hundreds of times, in week one. Every implicit conversion you have relied on for years is gone. "5" + 5 is an error. if 1 is an error. An i32 does not become an i64 by being used where one is wanted. There is no truthiness. This is not a deep conceptual problem — it is a volume problem, and it passes, but the first week is genuinely dispiriting and you should expect it rather than conclude something is wrong with you.
Where you stall, second: exhaustiveness. match must cover every case, and the compiler will name the one you forgot. This is initially annoying and eventually the thing you miss most.
Where you stall, third, and much later: ownership — but by then you will have built enough type intuition that it lands as one more rule rather than as a new universe.
What transfers, and it is more than you think: iterators. If you are fluent with comprehensions, generators and itertools, then .iter().filter().map().collect() is your native language, and Rust’s iterators are lazy in exactly the way generators are. Closures transfer. Pattern matching, if you have used it, transfers directly. Cargo will feel like a pip that works.
One concrete trap to note now, because it costs people real correctness: -7 % 2 is 1 in Python and -1 in Rust. Item 1.7 is about exactly this, and about rem_euclid, which is the fix nobody discovers on their own.
If you are coming from Haskell, OCaml or F
You have the easiest time with the type system and the strangest time with everything around it.
What transfers: sum types, pattern matching, exhaustiveness, Option and Result as ordinary values, typeclasses mapping onto traits, ? as a specialised bind, iterators as lazy sequences, immutability by default. Result<T, E> and ? will look like Either and a monadic bind, because they are.
Where you stall: ownership and lifetimes have no analogue in a garbage-collected functional language, and they interact with abstraction in ways that will feel arbitrary. Closures capture by move, by reference, or by mutable reference, and which one you got is part of the type — Fn, FnMut, FnOnce. Higher-kinded types do not exist, so the monad transformer stack you were about to write does not translate. And dyn Trait is not forall; the rules about which traits can even be made into dyn objects (Track 12) are a real constraint with real consequences.
What to actually do about it
Four things, all boring, all effective.
Read the whole error message. Not the headline. The underline label names both types; the help: line is often the literal fix. This is the single highest-leverage habit available to you and item 0.2 is entirely about it.
Predict the error before you compile. When a problem here tells you the starter fails with E0382, guess where first. Being right is what turns the compiler from an obstacle into a colleague, and being wrong is more informative than not guessing.
Do not fight the borrow checker in week one. Clone. Return owned data. Store indices instead of references. All three are legitimate engineering answers, not just training wheels — plenty of production Rust does all three deliberately. The course front-loads these escape hatches so there is always an exit, and then takes them away one at a time when you can afford it.
Expect the plateau. The consensus is roughly: a month to productive, three to comfortable, a year to fluent. The middle of that is a plateau where everything compiles and nothing feels elegant. It ends.