Wer in Rust produktiv werden will, muss eine Fertigkeit beherrschen, die in anderen Sprachen Nebensache ist: Fehlermeldungen lesen. Der rustc-Compiler ist gerade deshalb so streng, weil er dir mit jeder abgelehnten Compilation eine Lehre erteilt — Ownership-Verletzungen, fehlende Lifetimes, falsche Trait-Bounds. Die gute Nachricht: rustc-Diagnostik ist außergewöhnlich gut. Mit der richtigen Lesetechnik wird jedes Fehlerproblem zu einer Spur, die direkt zur Lösung führt. Dieser Artikel zerlegt eine rustc-Meldung in ihre Bauteile, geht die wichtigsten Error-Codes durch, zeigt, wann clippy mehr lehrt als rustc, und welche Werkzeuge dir im Hintergrund noch helfen.

Anatomie einer rustc-Fehlermeldung

Eine typische Meldung sieht so aus:

Rust rustc-Diagnose
error[E0382]: borrow of moved value: `s`
 --> src/main.rs:5:20
  |
3 |     let s = String::from("Hallo");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
4 |     consume(s);
  |             - value moved here
5 |     println!("{}", s);
  |                    ^ value borrowed here after move
  |
  = note: `consume` takes ownership of `s` because `String` does not implement `Copy`
  = help: consider cloning the value if the performance cost is acceptable: `s.clone()`

Diese Meldung hat sieben strukturelle Bauteile, und jedes davon hilft:

  1. error vs. warning — der Schweregrad. warning führt nicht zum Build-Abbruch (außer du erzwingst es mit -D warnings).
  2. [E0382] — der Error-Code. Eindeutig pro Fehlerart, recherchierbar mit rustc --explain E0382.
  3. borrow of moved value: \s`` — die Hauptaussage in einem Satz. Was ist konzeptuell falsch.
  4. --> src/main.rs:5:20 — die primäre Stelle: Datei, Zeile, Spalte.
  5. Code-Snippet mit ^-Markern — der primary span. Die Stelle, an der der Fehler wirklich auftritt, plus Kontext-Zeilen.
  6. note:Hintergrund. Warum genau der Compiler die Operation ablehnt.
  7. help: und suggestion:Lösungsvorschläge. Manchmal sogar mit konkretem Diff (note: this can be a Foo<T> if you do X).

Der entscheidende Reflex: immer von oben nach unten lesen. Erst der Error-Code-Satz, dann die Stelle, dann die note, dann die help. Oft braucht man die help-Zeile nicht — die note macht den Fehler schon verständlich.

Mit --explain mehr lernen

Für jeden Error-Code gibt es eine ausführliche Erklärung:

Rust Erklärung anfordern
rustc --explain E0382

Das öffnet eine mehrere Absätze lange Erklärung mit Beispiel-Code und Lösung. Bei Konzepten wie Ownership, Lifetimes oder Trait-Bounds extrem lehrreich — wer fünf solche Erklärungen liest, hat ein Drittel des Lernpensums hinter sich.

Die wichtigsten Error-Codes für Einsteiger

E0382 — borrow/use of moved value

Du hast einen Wert in eine Funktion (oder Variable) gemoved und versuchst danach, ihn nochmal zu verwenden.

Rust E0382 — typisches Beispiel
fn main() {
    let s = String::from("Hallo");
    consume(s);
    println!("{}", s);   // E0382 — s wurde gemoved
}

fn consume(_text: String) {}

Lösungen je nach Intent:

  • Wenn die Funktion den Wert nur lesen muss: per Referenz übergeben (consume(&s), Signatur fn consume(text: &String)).
  • Wenn beide den Wert brauchen: klonen (consume(s.clone())) — verständlich teuer.
  • Wenn du tatsächlich Ownership übergeben willst und danach nicht mehr brauchst: den println! weglassen.

E0502 — cannot borrow as mutable / as immutable

Du hast gleichzeitig eine shared (&) und eine mutable (&mut) Referenz im selben Scope.

Rust E0502
fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];        // shared borrow
    v.push(4);                // E0502 — mutable borrow nicht erlaubt
    println!("{}", first);
}

Rust verbietet Aliasing-und-Mutation gleichzeitig. Das ist der Kern der Memory-Safety-Garantie. Lösungen:

  • Den shared-Borrow vor dem mutable-Aufruf droppen (z. B. den println! davor verschieben, sodass first nach push nicht mehr gebraucht wird — NLL erkennt das ab Edition 2018).
  • Den Wert klonen statt referenzieren: let first = v[0].clone();.

E0507 — cannot move out of a borrowed value

Du versuchst, einen Wert aus einer Referenz herauszunehmen — was unmöglich ist, weil der Eigentümer dann ein leeres Loch hätte.

Rust E0507
struct Container { data: String }

fn extract(c: &Container) -> String {
    c.data            // E0507 — data ist hinter &Container
}

Lösungen:

  • Klonen: c.data.clone().
  • Den Container by-value annehmen: fn extract(c: Container) -> String { c.data }.
  • Bei mutable Borrow: std::mem::take(&mut c.data) ersetzt mit dem Default-Wert.

E0596 — cannot borrow as mutable

Du versuchst, eine &mut-Referenz von einer immutable Variable zu nehmen.

Rust E0596
fn main() {
    let v = vec![1, 2, 3];     // ohne `mut`
    v.push(4);                 // E0596 — v ist nicht mutable
}

Lösung: let mut v = ...;. Der Compiler-Suggestion-Block sagt das auch direkt.

E0308 — mismatched types

Der Klassiker. Du hast einen Wert vom Typ A, der Compiler erwartet Typ B.

Rust E0308
fn addiere(a: i32, b: i32) -> i32 {
    a + b;            // E0308 — Semikolon macht aus a+b den Wert ()
}

Der note: implicitly returns () as its body has no tail expression weist dich auf das Semikolon hin.

Mehrere Fehler gleichzeitig

rustc bricht nicht beim ersten Fehler ab — er sammelt mehrere und zeigt sie zusammen. Mit cargo build siehst du oft fünf, zehn Diagnosen auf einmal.

Strategie:

  1. Immer von oben nach unten beheben. Spätere Fehler folgen oft aus früheren (eine fehlende Variable führt zu allen ihren Verwendungs-Stellen, die auch melden).
  2. Nach jedem Fix neu bauen. Nicht versuchen, drei Fehler gleichzeitig im Kopf zu lösen — der nächste Build zeigt dir, ob die Top-Fehler verschwunden sind und welche neuen Fehler aufgetaucht sind.
  3. cargo check statt cargo build während du fixt. Faktor 5–10 schneller, weil keine Binary erzeugt wird.

warnings: ernst nehmen, ernst lassen

Warnings vom Compiler sind in Rust deutlich besser kuratiert als in C oder C++. Wenn rustc warnt, hat das meistens einen guten Grund. Häufige Beispiele:

  • unused_variables — du hast eine Variable angelegt, aber nie gelesen. Wenn das Absicht ist (z. B. bei einem Function-Parameter, der noch nicht genutzt wird), präfixe sie mit _: _unused.
  • unused_must_use — du ignorierst einen Wert (typischerweise ein Result), den der Compiler für wichtig hält. Lösung: entweder mit let _ = ... explizit ignorieren oder behandeln.
  • dead_code — eine Funktion oder ein Modul wird nirgendwo aufgerufen. Im Library-Code oft falsch positiv (sie wird ja von externen Crates aufgerufen) — dann mit pub markieren oder mit #[allow(dead_code)] explizit erlauben.

In CI: cargo build -- -D warnings macht alle Warnings zu Errors. Das verhindert, dass sich Warnings im Code anlagern.

Clippy — der Linter, der unterrichtet

cargo clippy ist mehr als ein Linter — es ist ein Lehrmittel. Über 700 Lints, jeder mit einer kurzen Erklärung, viele mit automatischer Korrektur.

Rust Clippy ausführen
cargo clippy                          # Standard-Lints
cargo clippy -- -W clippy::pedantic   # Pedantischer Modus, viele Hinweise mehr
cargo clippy --fix                    # Wo möglich, automatisch fixen

Beispiel-Lint:

Rust Clippy-Diagnose
warning: this `if let` can be collapsed into the outer `match`
 --> src/main.rs:8:5
  |
8 |     if let Some(x) = opt { println!("{x}"); }
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
help: replace it with this
  |
8 |     opt.map(|x| println!("{x}"));
  |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~
  |
  = note: `#[warn(clippy::manual_map)]` on by default

Jeder Lint hat einen Namen (clippy::manual_map), eine Begründung im Lint-Verzeichnis und oft eine spielbare Korrektur. Du kannst einzelne Lints lokal abschalten:

Rust Lint ausschalten
#[allow(clippy::manual_map)]
fn main() {
    // ...
}

Für die Code-Qualität in einem Projekt ist es empfehlenswert, cargo clippy -- -D warnings in CI laufen zu lassen. Wer Clippy von Anfang an ernst nimmt, lernt schneller idiomatisches Rust als jeder, der nur rustc-Errors fixt.

rust-analyzer in der IDE

In VS Code, Neovim, Helix oder JetBrains übernimmt rust-analyzer die rustc-Diagnostik live während du tippst. Was er zeigt:

  • Inline-Errors und -Warnings — kein Build nötig.
  • Type-Hints: über jeder let-Bindung kannst du den inferred Typ einblenden.
  • Inlay-Hints für Closure-Parameter, generische Argumente, Iterator-Adapter.
  • Hover-Doku: Mauszeiger über einen Identifier zeigt seine Doc-Kommentare.
  • Quick Fixes: viele rustc-Suggestions sind als Ein-Klick-Action verfügbar.
  • Go-to-Definition auch in std hinein, wenn rust-src installiert ist.

Rust ohne rust-analyzer ist möglich, aber etwa wie TypeScript ohne tsserver — funktioniert, lässt aber viel Arbeitserleichterung liegen.

cargo expand — Makros sichtbar machen

Wenn du eine Makro-Expansion verstehen willst (z. B. was #[derive(Debug)] oder println! wirklich generiert), nutze das Tool cargo-expand:

Rust Installation und Verwendung
cargo install cargo-expand          # einmalig
cargo expand                        # Gesamtes Crate
cargo expand --bin meine-app
cargo expand --lib path::to::module

Sehr nützlich, wenn ein derive-Makro ungewöhnliches Verhalten zeigt oder du selbst proc-macros baust. Auch unentbehrlich beim Lesen von Crates, die intensiv mit Makros arbeiten (z. B. serde, tokio).

cargo check vs. cargo build im Workflow

Wiederholung, weil so wichtig:

BefehlWas er tutWann nutzen
cargo checkParsen, Typchecken, Borrow-Checken, kein LinkingWährend der Entwicklung, in einer Watch-Loop
cargo buildWie check + Codegen + LinkingWenn du eine Binary brauchst
cargo clippyWie check + LintsVor jedem Commit
cargo testBuild + Tests laufenVor jedem Commit

Empfohlenes Live-Setup:

Rust Watch-Loop
cargo install cargo-watch
cargo watch -x check                              # Bei jeder Datei-Änderung check
cargo watch -x check -x test                      # check + test
cargo watch -x 'clippy -- -D warnings'            # Strenge Lints

cargo-watch startet bei jeder Datei-Änderung neu — du tippst, speicherst, und siehst sofort, ob du etwas kaputt gemacht hast. Faktor 10 für die Lernkurve.

FAQ

Warum sind rustc-Meldungen so lang?

Weil sie dir den Kontext mitliefern. Andere Compiler sagen „type error" — rustc zeigt dir die Stelle, den Typ-Unterschied, oft den Grund (note) und meist sogar einen Fix (help). Was anfangs überwältigend wirkt, ist nach ein paar Wochen das Werkzeug, mit dem du schneller wirst.

Sollte ich unwrap() verwenden, weil der Compiler nicht meckert?

In Beispielen ja, in produktivem Code nein. unwrap() panickt zur Laufzeit, wenn der Option oder Result None/Err ist. Clippy warnt mit clippy::unwrap_used und clippy::expect_usedexpect("Begründung") ist immerhin selbstdokumentierend, aber idealerweise behandelst du den Fehler-Fall explizit (siehe Error-Handling-Kapitel).

Was tun, wenn ich die Fehlermeldung nicht verstehe?

Drei Strategien: (1) rustc --explain E0xxx für die ausführliche Erklärung. (2) Den Error-Code googeln — die Rust-Community schreibt viel über die typischen Fallen. (3) Den Fall minimieren — strip alles raus, bis nur der Fehler übrig ist; oft erkennt man dann das Pattern.

Warum kompiliert es nach einem rustc-Update auf einmal nicht mehr?

Selten ein Sprach-Bruch — fast immer ein Lint, der von Warning zu Error befördert wurde, oder eine Dependency, die strenger geprüft wird. cargo build -- -W unstable-features zeigt unstable-Verwendungen. cargo update und das Cargo.lock neu schreiben löst manche Inkonsistenzen. Bei echten Sprach-Brüchen würde das Compatibility Promise verletzt sein — das ist äußerst selten.

Lints in Cargo.toml projektweit konfigurieren — geht das?

Ja. Das [lints]-Tabellen-Feature seit Rust 1.74 erlaubt projektweite Lint-Konfiguration im Manifest: [lints.rust] unused_imports = "deny" oder [lints.clippy] manual_map = "warn". Praktisch, weil sonst alle CI-Aufrufe Flag-Listen mitschleppen.

Sind die help:-Vorschläge immer richtig?

Meistens schon, aber nicht immer. Die help-Zeilen sind heuristisch — gerade bei komplexeren Lifetime- oder Generics-Fehlern schlägt rustc manchmal eine Lösung vor, die das Symptom heilt, aber nicht die Ursache. Verstehen, dann anwenden.

Wie zeige ich nur Errors, keine Warnings?

cargo build 2>&1 | grep -A 5 ^error filtert auf der Shell. In rust-analyzer kannst du die Diagnostik-Severity in den Einstellungen filtern. Für gezielte Suche nach einem bestimmten Code: cargo build 2>&1 | grep -A 10 E0382.

Was ist mit panic!-Meldungen zur Laufzeit?

Die unterscheiden sich grundsätzlich von Compile-Fehlern. Eine Panic-Meldung wie thread 'main' panicked at 'index out of bounds: ...' ist ein Runtime-Crash. Mit RUST_BACKTRACE=1 cargo run bekommst du den vollen Stack-Trace. Ein panic deutet meist auf einen logischen Fehler oder ein nicht behandeltes Edge-Case — der Borrow Checker findet ihn nicht im Voraus.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Grundlagen

Zur Übersicht