Eine divergierende Funktion ist eine Funktion, die niemals zurückkehrt — sie panickt, beendet den Prozess, läuft endlos oder verzweigt anders. In Rust drückt man das mit dem Rückgabe-Typ ! (Never) aus. Der Compiler erkennt damit: nach einem Aufruf dieser Funktion gibt es keinen weiteren Code-Pfad. Praktisch nutzbar wird das durch die Coercion-Eigenschaft des Never-Type — er fügt sich in jeden anderen Typ ein, sodass divergierende Calls in match-Armen, if-Branches oder Zuweisungen stehen dürfen, ohne Typ-Konflikte zu erzeugen.
Die Syntax: -> !
fn fataler_fehler(msg: &str) -> ! {
eprintln!("FATAL: {msg}");
std::process::exit(1);
}
fn main() {
let n: i32 = match "abc".parse() {
Ok(v) => v,
Err(_) => fataler_fehler("kein Integer"),
};
println!("{n}");
}Drei Beobachtungen:
-> !als Rückgabe-Typ — der Compiler weiß: diese Funktion kehrt nie zurück.- Im
match-Arm liefert derErr-Branch eineni32-Wert (für die Zuweisung ann), obwohlfataler_fehlerkeinen liefert. Der!-Typ coerciert zu jedem anderen Typ. - Der Code nach
fataler_fehler(...)ist unerreichbar — keine Warnung nötig.
Im Unit-und-Never-Artikel des Primitive-Datentypen-Kapitels wurde der Never-Type schon eingeführt; dieser Artikel fokussiert auf seine Anwendung in Funktions-Signaturen.
Wie der Compiler ! „herstellt"
Drei Wege, eine -> !-Funktion zu schreiben:
1. Über panic!
fn unbekannte_variante(name: &str) -> ! {
panic!("unbekannte Variante: {name}");
}panic! selbst hat Typ !. Eine Funktion, die nur das tut, ist automatisch divergent.
2. Über process::exit oder process::abort
use std::process::exit;
fn beenden_mit_code(code: i32) -> ! {
eprintln!("Beende mit Code {code}");
exit(code);
}std::process::exit(i32) und std::process::abort() haben Typ !. Beide beenden den Prozess sofort.
3. Über endlose Schleifen
fn server_loop() -> ! {
loop {
// Handle requests forever
}
}Ein loop ohne break (und ohne return) hat Typ ! — der Compiler weiß: hier kommt nichts mehr zurück.
Coercion in beliebige Typen
Das Schlüsselverhalten:
fn abbruch() -> ! {
panic!("Stop");
}
fn main() {
// ! coerciert zu i32:
let a: i32 = if false { 5 } else { abbruch() };
// ! coerciert zu String:
let b: String = match "x" { _ => abbruch() };
// ! coerciert zu fn-Rückgabe:
fn beispiel(b: bool) -> u8 {
if b { 42 } else { abbruch() }
}
}! ist der bottom type des Type-Systems: jeder Typ ist Supertype von !. Damit fügt sich ein divergierender Call überall ein.
Was zählt als divergent?
Eine Funktion ist genau dann divergent, wenn der Compiler beweisen kann, dass sie keinen Pfad zurück zum Aufrufer hat. Konkret:
- Funktion endet immer mit
panic!,process::exit,process::abort. - Endet mit
loop { ... }ohnebreak/return. - Endet mit einem
match, in dem alle Arme divergent sind. - Endet mit einem rekursiven Aufruf einer divergenten Funktion (sehr selten).
Wenn nur manche Pfade divergent sind und andere zurückkehren, ist die Funktion nicht -> !. Beispiel:
// Nicht ! — manche Pfade kehren zurück
fn pruefe(n: i32) -> i32 {
if n < 0 {
panic!("negativ"); // ! im Branch
}
n * 2 // kehrt zurück
}Hier kehrt die Funktion bei positivem n mit i32 zurück — sie ist -> i32, nicht -> !.
Infallible als verwandter Typ
Wenn du eine fallible API entwirfst, deren Implementierung aber niemals fehlschlägt, gibt es std::convert::Infallible — ein leerer Enum, der wie ! funktioniert:
use std::convert::Infallible;
use std::str::FromStr;
struct AlwaysOk(String);
impl FromStr for AlwaysOk {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(AlwaysOk(s.to_string())) // Niemals Err
}
}Infallible ist ein Enum ohne Varianten — es kann niemals einen Wert haben. Daher ist Result<T, Infallible> immer Ok, und .unwrap() darauf ist statisch unfehlbar.
Stand 2026 wird ! zunehmend als Type-Parameter stabilisiert; bis dahin ist Infallible der vollständig stabile Stand-in.
Praxis: Divergierende Funktionen im echten Code
Fataler Logger mit sauberem Abbruch
pub fn fatal(msg: impl AsRef<str>) -> ! {
eprintln!("FATAL: {}", msg.as_ref());
eprintln!("Programm wird beendet.");
std::process::exit(1);
}
fn main() {
let pfad = std::env::var("CONFIG_PATH")
.unwrap_or_else(|_| fatal("CONFIG_PATH nicht gesetzt"));
println!("Lade Konfig aus: {pfad}");
}fatal wird in unwrap_or_else als Closure verwendet — passt typmäßig, weil ! zu String coerciert. Idiomatischer als ein verbose match mit Err(_) => { eprintln!(...); std::process::exit(1) }.
Server-Daemon mit infinite Loop
use std::net::TcpListener;
pub fn serve_forever(addr: &str) -> ! {
let listener = TcpListener::bind(addr)
.unwrap_or_else(|e| panic!("Bind fehlgeschlagen: {e}"));
loop {
match listener.accept() {
Ok((stream, _peer)) => {
std::thread::spawn(move || handle(stream));
}
Err(e) => eprintln!("Accept-Fehler: {e}"),
}
}
}
# fn handle<S>(_: S) {}-> ! signalisiert: dieser Aufruf kehrt nie zurück. Wer serve_forever(...) ruft, kann danach keinen weiteren Code mehr in derselben Funktion erwarten.
Unreachable-Marker in match
pub fn priorisieren(rolle: &str) -> u8 {
match rolle {
"admin" => 0,
"operator" => 10,
"user" => 100,
_ => unreachable!("unbekannte Rolle: {rolle}"),
}
}unreachable!() ist ein Makro mit Typ ! — der Match-Arm coerciert zu u8. Sehr nützlich für Cases, die logisch unmöglich sind, aber der Compiler doch eine Vollständigkeits-Garantie braucht.
Skeleton-API mit todo
pub trait Storage {
fn lese(&self, key: &str) -> String;
fn schreibe(&mut self, key: &str, wert: &str);
}
pub struct PostgresStorage;
impl Storage for PostgresStorage {
fn lese(&self, _key: &str) -> String {
todo!("Postgres-Lookup implementieren")
}
fn schreibe(&mut self, _key: &str, _wert: &str) {
todo!()
}
}todo!() ist ein Makro mit Typ ! — der Compiler stört nicht beim Implementieren des Skeletts. Erst beim Aufruf zur Laufzeit panickt es.
Custom Assert mit !-Helper
fn assert_pos_with_context(wert: i64, context: &str) -> u64 {
if wert >= 0 {
wert as u64
} else {
panic!("Erwarte positive Zahl in {context}, bekam {wert}");
}
}Der else-Branch ist divergent (panic!) — der Match coerciert zu u64. Eleganter als Helper-Macros für einfache Checks.
Process-Watchdog
use std::time::{Duration, Instant};
use std::thread::sleep;
pub fn beobachte_und_beende_nach(deadline: Instant) -> ! {
loop {
if Instant::now() >= deadline {
eprintln!("Deadline erreicht — beende Prozess");
std::process::exit(124); // 124 = klassisch für Timeout
}
sleep(Duration::from_secs(1));
}
}Echter Daemon-Code: läuft endlos, beendet sich nur extern (Signal, Timeout). -> ! macht das Verhalten im Typ-System sichtbar.
Sentinel-Wert mit Infallible-Result
use std::convert::Infallible;
use std::str::FromStr;
pub struct AlwaysSafe(String);
impl FromStr for AlwaysSafe {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(AlwaysSafe(s.to_string()))
}
}
fn main() {
// .unwrap() ist statisch sicher — Err existiert nicht
let s: AlwaysSafe = "hi".parse().unwrap();
assert_eq!(s.0, "hi");
}Für APIs, die einen generischen Result<T, E> zurückgeben müssen, obwohl konkret kein Fehler entstehen kann.
Besonderheiten
! ist als Rückgabe-Typ stable.
Seit Rust 1.26 (Anfang 2018) ist -> ! als Rückgabe-Typ stable. Als generischer Type-Parameter (z. B. Result<T, !>) ist es teilweise noch nightly — die meisten Anwendungen kommen mit dem stable Subset aus. Infallible füllt die Lücke.
panic!, unreachable!, todo!, unimplemented! sind alle !-Makros.
Alle vier panicen zur Laufzeit. Semantik unterschiedlich: panic! für echte Fehler, unreachable! für logisch unerreichbare Code-Pfade, todo! und unimplemented! als Marker während der Entwicklung. Alle haben Typ ! und coercen sich in jeden Kontext.
! ermöglicht Type-Konsistenz im match.
Ein match-Arm mit panic!() darf in einer Funktion stehen, die ansonsten i32 zurückgibt. Ohne !-Coercion müsste jeder Arm einen i32 liefern — was bei Panic-Branches unsinnig wäre. ! macht das Typ-System konsistent.
std::process::abort vs. exit.
exit(code) ruft destructors für statics, schreibt stdout-Buffer, beendet sauber. abort() ist sofortiger Prozess-Kill ohne Cleanup. Beide haben -> !. Faustregel: exit für „normale" Beendigung, abort für „panic-im-panic" oder echte Notfälle.
Server-Funktionen mit -> ! erleichtern main-Design.
fn main() -> ! ist legal — wenn main die Server-Loop selbst hostet und nie zurückkehrt, kann es so deklariert sein. Aber fn main() -> Result<(), E> ist meistens hilfreicher, weil ? benutzbar wird.
Infallible ist ! in Stable-Verkleidung.
Strukturell ein leerer Enum, semantisch identisch zu !. Wer eine Trait-Impl für „kann nie fehlschlagen" schreibt, nimmt type Err = Infallible. Der Compiler erkennt das und erlaubt .unwrap() ohne Warnung über mögliche Panics.
Code nach -> !-Aufrufen ist unreachable.
rustc warnt mit unreachable_code bei toten Statements nach einem divergierenden Call. Sehr hilfreich beim Refactoring — wenn du eine Funktion zu -> ! machst, zeigt der Compiler dir alle Stellen, an denen vorher noch Code stand.
loop { }-Body als !-Funktion ist häufiges Embedded-Pattern.
Auf Microcontrollern hat die main-Funktion oft Rückgabe ! — es gibt kein „nach main", weil keine OS-Runtime auf eine Rückkehr wartet. #![no_main] und fn main() -> ! mit einem loop-Body sind Standard-Setup in Embedded-Rust.
Weiterführende Ressourcen
Externe Quellen
- Rust Reference – Never type
- std::primitive.never
- std::convert::Infallible
- std::process::exit
- std::process::abort
- Rust RFC 1216 – Bang Type