Zwei Typen sitzen in Rust an besonderen Stellen des Typ-Systems: Unit (()) — das leere Tupel, das „kein interessanter Wert" ausdrückt — und Never (!) — der Typ für Ausdrücke, die niemals zurückkehren. Beide tauchen überall im Code auf, oft unsichtbar. Wer sie versteht, versteht, warum panic! als Wert in einer Expression stehen darf, warum eine Funktion ohne return „eigentlich () zurückgibt" und warum loop {} jeden Typ erfüllen kann. Dieser Artikel erklärt beide.

Unit — das leere Tupel

Der Unit-Typ ist geschrieben als () — runde Klammern ohne Inhalt. Er hat genau einen Wert, ebenfalls geschrieben (). Typ und Wert haben die gleiche Notation.

Rust Unit
let nichts: () = ();
println!("Size: {}", std::mem::size_of::<()>());  // 0

Unit belegt 0 Bytes im Speicher. Es transportiert keine Information — es ist die Information „hier gibt es nichts Sinnvolles".

Wo Unit überall auftaucht

  • Rückgabetyp von Funktionen ohne expliziten Return-Type:
Rust Implicit Unit
fn drucke(msg: &str) {           // Rückgabetyp implizit ()
    println!("{msg}");
}
let x: () = drucke("Hi");
  • Ergebnis von Statements: ein let-Statement, ein Expression-Statement mit Semikolon — alles ist ().
  • Leere Blocks: let x = {}; ergibt x: ().
  • while- und for-Loops: keine Wert-Rückgabe — Typ ().
  • Result<(), E>: ein Result, dessen Erfolgswert nichts Interessantes bedeutet (z. B. „Datei wurde geschrieben"). Sehr häufiges Pattern.
Rust Result mit Unit
fn schreibe(daten: &str) -> Result<(), std::io::Error> {
    std::fs::write("output.txt", daten)?;
    Ok(())
}

Ok(()) — der Erfolgsfall enthält nichts außer der Information „erfolgreich".

Never — der Typ, der nie zurückkehrt

! (gesprochen „never") ist der Typ für Ausdrücke, die niemals einen Wert produzieren — weil sie panicen, terminieren, oder in einer Endlosschleife laufen.

Rust Never-Beispiele
// panic! gibt Typ !
fn unmoeglich() -> ! {
    panic!("Das hätte nicht passieren dürfen");
}

// loop ohne break gibt Typ !
fn endlos() -> ! {
    loop {
        println!("immer noch da");
    }
}

// std::process::exit gibt !
fn beende() -> ! {
    std::process::exit(1);
}

! ist der bottom type im Type-Theory-Sinn: ein Typ ohne Werte. Es gibt keinen let x: ! = ...;, weil es nichts gibt, was du da reinpacken könntest.

Die Magie von Never: Coercion in jeden Typ

Der eigentliche Trick: ! lässt sich zu jedem anderen Typ coercen. Das ist konsistent — wenn ein Ausdruck nie zurückkehrt, kann er „so tun", als ob er einen Wert beliebigen Typs liefere, weil er das ja sowieso nie tun wird.

Rust ! als beliebiger Typ
fn parse_oder_panic(s: &str) -> i32 {
    match s.parse() {
        Ok(n) => n,
        Err(_) => panic!("Kein Integer: {s}"),
    }
}

Hier sind die zwei Match-Arme:

  • Ok(n) => n — Typ i32.
  • Err(_) => panic!(...) — Typ !.

Damit match einen einheitlichen Typ haben kann, coerciert der Compiler ! zu i32. Aus seiner Sicht: der Err-Arm liefert „nie", also gibt es keinen Konflikt.

Das gleiche Pattern bei if/else:

Rust if/else mit !
fn lese_konfig(input: &str) -> u32 {
    let n: u32 = if input.is_empty() {
        panic!("Eingabe leer")
    } else {
        input.parse().expect("Kein Integer")
    };
    n
}

Auch hier: der if-Branch hat Typ !, der else-Branch u32. Coercion macht beide kompatibel.

Wo Never überall auftaucht

  • panic!, unreachable!, todo!, unimplemented! — alle Marker-Makros haben Rückgabe-Typ !.
  • return ...; als Expression hat Typ ! (du verlässt die Funktion).
  • break ...; als Expression in loop hat Typ ! (du verlässt die Schleife).
  • continue; ebenfalls !.
  • loop ohne break!.
  • std::process::exit(...), std::process::abort()!.
Rust return und break als !
fn finde(slice: &[i32], ziel: i32) -> Option<usize> {
    for (i, &v) in slice.iter().enumerate() {
        if v == ziel {
            return Some(i);     // return ist ein !-Ausdruck, das ist okay
        }
    }
    None
}

return Some(i) ist syntaktisch ein Ausdruck — sein Typ ist !, weil danach nichts mehr in dieser Funktion läuft. Dass if v == ziel einen if-Block ohne else hat, ist nur deshalb möglich, weil return ... Typ ! hat und damit zu () coerciert.

Status von Never als Type

Eine technische Eigenheit: ! ist als eigener Typ im Type-System lange Zeit instabil gewesen. Die Coercion-Logik ist stable (du kannst panic! in einer i32-Position nutzen), aber ! als expliziter Type in Signaturen (fn foo() -> !) ist seit Rust 1.26 (2018) stable.

Stand 2026: ! ist in den meisten Kontexten produktiv nutzbar:

Rust Never als Type
fn explode() -> ! {
    panic!("boom");
}

// Funktioniert auch — zukünftige Compatibility
type Niemals = !;

Wo ! noch nicht überall hin darf (z. B. als Typ-Parameter Result<T, !>), nutzt man oft std::convert::Infallible als „Stand-in" — ein leerer enum mit der gleichen Eigenschaft.

Rust Infallible-Workaround
use std::convert::Infallible;

fn niemals_fehlerhaft() -> Result<i32, Infallible> {
    Ok(42)
}

Infallible und ! sind funktional äquivalent — beide sind Typen ohne Werte. Mit zunehmender Stabilisierung wird ! zunehmend bevorzugt.

Unit und Never im Vergleich

AspektUnit ()Never !
Anzahl Werte1 (nämlich ())0 (keine Werte)
Speichergröße0 Bytesn/a (kann nicht existieren)
Bedeutung„kein interessanter Wert"„dieser Ausdruck endet nie normal"
Coercionnicht zu beliebigem Typzu jedem Typ
Typisches VorkommenFunktions-Returns ohne Wert, Result<(), E>panic!, loop {}, exit()
Wert konstruierbar?ja (())nein

Beide sind essentiell für ein konsistentes Type-System:

  • Unit ist „identitäts-Wert" für ResultResult<(), E> ist die Standard-Form für „erfolg ohne Daten".
  • Never ist „identitäts-Wert" für Type-Coercion — Branches, die nicht zurückkehren, müssen das ausdrücken können.

Praxis: Wo Unit und Never im echten Code stehen

Datei-Schreibe-Operation mit Result<(), Error>

Die meisten I/O-Operationen liefern „Erfolg ohne Daten" oder „Fehler" — perfekt für Result<(), E>:

Rust Atomic-Write
use std::fs;
use std::path::Path;
use std::io;

fn schreibe_atomisch(pfad: &Path, daten: &[u8]) -> Result<(), io::Error> {
    let temp = pfad.with_extension("tmp");
    fs::write(&temp, daten)?;
    fs::rename(&temp, pfad)?;
    Ok(())
}

fn main() -> Result<(), io::Error> {
    schreibe_atomisch(Path::new("config.json"), b"{}")?;
    Ok(())
}

Ok(()) ist hier der idiomatische Erfolgs-Wert — die Funktion hat erfolgreich getan, was sie sollte, ohne dass es etwas zurückzugeben gibt.

Server-Loop, der nie zurückkehrt — -> !

Ein klassischer Daemon, der bis zum Prozess-Ende läuft:

Rust Server-Daemon
use std::net::TcpListener;

fn server_loop(addr: &str) -> ! {
    let listener = TcpListener::bind(addr).expect("Bind fehlgeschlagen");
    loop {
        match listener.accept() {
            Ok((stream, peer)) => {
                std::thread::spawn(move || handle_client(stream, peer));
            }
            Err(e) => eprintln!("Accept-Fehler: {e}"),
        }
    }
}
# fn handle_client<S, P>(_: S, _: P) {}

Die Funktion hat Rückgabetyp ! — der Compiler weiß: hinter dem Aufruf kommt nichts mehr. In der main-Funktion kann dieser Aufruf an einer Position stehen, wo eigentlich ein anderer Typ erwartet wird, ohne dass match-Type-Konflikte auftreten.

Frühe Validierung mit panic! oder todo!

Während der Entwicklung tauchen todo!() und unimplemented!() überall auf:

Rust Skeleton-Implementierung
struct Datenbank;

impl Datenbank {
    fn finde_user(&self, _id: u64) -> User {
        todo!("Datenbank-Lookup implementieren")
    }

    fn anzahl_aktiv(&self) -> usize {
        unimplemented!("Aggregations-Query noch nicht designt")
    }
}
# struct User;

Beide Makros haben Typ ! und coercen sich in jeden Return-Typ. So kann man das Skeleton einer API definieren, jeder Aufrufer kompiliert sauber — erst zur Laufzeit panickt der ungenutzte Pfad. Ideal für Top-Down-Entwicklung.

Ein Cancellation-Token mit Result<T, Cancelled>

Ein häufiges Concurrency-Pattern: lang laufende Berechnungen sollen abbrechbar sein.

Rust Cancellation
struct Cancelled;

fn lange_berechnung(check: impl Fn() -> bool) -> Result<u64, Cancelled> {
    let mut summe: u64 = 0;
    for i in 0..1_000_000 {
        if check() { return Err(Cancelled); }     // Result<T, !>-artig
        summe += i;
    }
    Ok(summe)
}

Wenn check() true liefert, gibt die Funktion Err(Cancelled) zurück — der return Err(...) ist ein !-Ausdruck, der den Funktions-Pfad beendet. Der Aufrufer sieht ein normales Result<u64, Cancelled>.

Logger::log mit Unit-Rückgabe

Logger geben in der Regel () zurück — sie haben einen Seiteneffekt, kein Resultat:

Rust Trait mit Unit-Return
trait Logger {
    fn info(&self, msg: &str);                    // -> ()
    fn warn(&self, msg: &str);
    fn error(&self, msg: &str);
}

struct StdoutLogger;

impl Logger for StdoutLogger {
    fn info(&self, msg: &str) { println!("[INFO] {msg}"); }
    fn warn(&self, msg: &str) { eprintln!("[WARN] {msg}"); }
    fn error(&self, msg: &str) { eprintln!("[ERR ] {msg}"); }
}

Die Methoden geben implizit () zurück. Klar lesbar, kein syntaktischer Overhead.

Besonderheiten

() ist der Default-Typ, wenn nichts anderes passt.

In einigen Inferenz-Situationen, in denen der Compiler einen Typ wählen muss und nichts ihn einschränkt, wählt er (). Häufigster Fall: das Resultat einer Funktion ohne expliziten Rückgabe-Typ. Auch let x; in deferred init ist initial vom Typ (), bis ein Pattern oder eine Annotation hilft.

panic!() in einer match-Arm-Position rettet die Type-Konsistenz.

Ohne den !-Type wäre match opt { Some(n) => n, None => panic!("...") } ein Type-Konflikt (i32 vs ()). Mit !-Coercion fügt sich der Panic-Arm sauber ein. Das ist eine der elegantesten Konsistenz-Eigenschaften des Rust-Typsystems.

?-Operator und ! hängen indirekt zusammen.

Der Fragezeichen-Operator (?) auf einem Result<T, E> macht eine implizite return Err(e.into()) im Fehlerfall — also einen !-typisierten Code-Pfad. Dass danach trotzdem mit dem T-Wert weitergearbeitet wird, klappt nur, weil ! zu T coerciert wird (oder vielmehr: weil der return einen !-Ausdruck darstellt, der den Code abbricht).

std::process::abort() ist nicht das gleiche wie std::process::exit().

Beide haben Rückgabe-Typ !. exit(code) läuft destructors für statics, schreibt stdout/stderr-Buffer, beendet sauber. abort() killt den Prozess sofort ohne Cleanup. Beide nützlich, aber in unterschiedlichen Szenarien — gerade in Tests oder Fatal-Error-Pfaden.

loop {} ohne break hat Typ !.

Eine Endlos-Schleife terminiert nie — also kehrt sie auch nie zurück. Der Compiler kennt das Muster und gibt dem Loop-Ausdruck Typ !. Du kannst eine solche Schleife also als Rückgabe einer ! -Funktion verwenden — oder als „Stand-in" für nicht implementierte Pfade, ähnlich wie todo!().

let _ = expr; ist anders als let x = expr;, wenn expr Drop-Type hat.

Nicht direkt mit Never zu tun, aber im Unit-Kontext relevant: let _ = guard(); droppt den MutexGuard (oder ähnlich) sofort. let _x = guard(); hält den Guard bis zum Scope-Ende. Häufige Quelle versehentlich freigegebener Locks.

todo!() und unimplemented!() für noch zu schreibenden Code.

Beide haben Typ ! und panicen zur Laufzeit. todo!() ist semantisch „dieses Stück fehle noch", unimplemented!() ist „dieses Stück implementiere ich nie". In der Praxis bevorzugt man todo!() während der Entwicklung — der Compiler beschwert sich nicht über fehlende Branches, weil ! in jeden Typ coerciert.

Infallible ist ! in Stable-Verkleidung.

Solange ! als Type-Parameter nicht überall stable ist (in 2026 immer noch in manchen Edge-Cases unstable), nutzt man std::convert::Infallible als Drop-in-Ersatz. Result<T, Infallible> ist ein Result, der nie fehlschlagen kann. Mit .unwrap() wird der Ok-Wert garantiert herausgeholt.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Primitive Datentypen

Zur Übersicht