Strings müssen in echtem Code laufend in andere Typen verwandelt werden: User-Input zu Zahlen, JSON zu Datenobjekten, Config-Werte zu Enums. Rust standardisiert diesen Vorgang über den FromStr-Trait und die Methode .parse(). Sie ist generisch über das Ziel und gibt immer einen Result<T, E> zurück — der Fehlerfall ist nicht versteckt. Dieser Artikel zeigt, wie parse() funktioniert, wie du eigene Typen parsefähig machst, wie der Rückweg über Display läuft und welche Patterns sich bei der Fehler-Komposition mit ? etabliert haben.

.parse() — das generische Tor

Jede &str hat die Methode parse::<T>(), die einen Result<T, T::Err> zurückgibt — vorausgesetzt, T implementiert FromStr.

Rust Grundlegende Anwendung
fn main() {
    let a: i32 = "42".parse().unwrap();              // 42
    let b: f64 = "3.14".parse().unwrap();            // 3.14
    let c: u8 = "255".parse().unwrap();              // 255
    let d: bool = "true".parse().unwrap();           // true

    // Mit Turbofish
    let e = "42".parse::<i32>().unwrap();

    // Fehler-Fall
    let f: Result<i32, _> = "abc".parse();
    assert!(f.is_err());
}

Was wird unterstützt? Alle primitiven Numbers (i8 bis i128, u8 bis u128, f32, f64), bool, char, IpAddr, SocketAddr, PathBuf, String (ja, String::from_str existiert) und viele andere Stdlib-Typen.

Der FromStr-Trait

FromStr ist die Spec:

Rust FromStr-Definition
trait FromStr: Sized {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

Zwei Dinge sind beachtenswert:

  • type Err ist ein assoziierter Typ — jedes FromStr-Impl legt seinen eigenen Error-Typ fest. Bei Integern ist es ParseIntError, bei Floats ParseFloatError, usw.
  • Sized — der Ziel-Typ muss eine bekannte Größe haben (also kein [T] oder dyn Trait).

.parse() ruft im Hintergrund einfach T::from_str(self) auf — parse ist also ein syntaktischer Convenience.

Fehler-Handhabung mit ?

Praktisch immer wird .parse() mit ? kombiniert, um Fehler nach oben zu reichen:

Rust ? statt unwrap
fn parse_alter(input: &str) -> Result<u8, std::num::ParseIntError> {
    let n: u8 = input.parse()?;
    Ok(n)
}

fn main() {
    match parse_alter("28") {
        Ok(a) => println!("Alter: {a}"),
        Err(e) => eprintln!("Fehler: {e}"),
    }
}

? macht aus einem Result<T, E> ein T (im Erfolgsfall) oder einen frühen Return mit Err(e) (im Fehlerfall). Voraussetzung: die Funktion gibt ihrerseits einen Result mit kompatiblem Error-Typ zurück.

Mit Box<dyn Error> — der bequeme Sammeltopf

Rust Mixed Errors
fn parse_record(zeile: &str) -> Result<(u32, f64), Box<dyn std::error::Error>> {
    let mut teile = zeile.split(',');
    let id: u32 = teile.next().ok_or("ID fehlt")?.parse()?;
    let preis: f64 = teile.next().ok_or("Preis fehlt")?.parse()?;
    Ok((id, preis))
}

fn main() {
    match parse_record("42,19.99") {
        Ok((id, p)) => println!("ID {id} kostet {p}"),
        Err(e) => eprintln!("Parse-Fehler: {e}"),
    }
}

Box<dyn std::error::Error> ist der Sammeltopf, in den jeder Error-implementierende Typ via ? einsortiert wird. Praktisch in Anwendungs-Code; in Library-Code lieber eigene Error-Typen (siehe Error-Handling-Kapitel).

Eigene FromStr-Implementierungen

Für Domänen-Typen lohnt sich oft ein eigener FromStr:

Rust Eigener Typ mit FromStr
use std::str::FromStr;

#[derive(Debug, PartialEq)]
enum Ampelphase { Rot, Gelb, Gruen }

#[derive(Debug)]
struct UnbekanntePhase(String);

impl FromStr for Ampelphase {
    type Err = UnbekanntePhase;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim().to_lowercase().as_str() {
            "rot" | "red" => Ok(Ampelphase::Rot),
            "gelb" | "yellow" => Ok(Ampelphase::Gelb),
            "grün" | "gruen" | "green" => Ok(Ampelphase::Gruen),
            other => Err(UnbekanntePhase(other.to_owned())),
        }
    }
}

fn main() {
    let p: Ampelphase = "ROT".parse().unwrap();
    assert_eq!(p, Ampelphase::Rot);

    let fehler: Result<Ampelphase, _> = "lila".parse();
    assert!(fehler.is_err());
}

Nach der Implementierung funktioniert "rot".parse::<Ampelphase>() und alle anderen parse()-Convenience.

Strukturierter Typ mit mehreren Feldern

Rust Strukturierter Parse
use std::str::FromStr;

#[derive(Debug)]
struct Version { major: u32, minor: u32, patch: u32 }

impl FromStr for Version {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let teile: Vec<&str> = s.trim().split('.').collect();
        if teile.len() != 3 {
            return Err(format!("Erwartete 3 Teile, bekam {}", teile.len()));
        }
        let parse = |t: &str| -> Result<u32, String> {
            t.parse().map_err(|e| format!("'{t}' ist kein u32: {e}"))
        };
        Ok(Version {
            major: parse(teile[0])?,
            minor: parse(teile[1])?,
            patch: parse(teile[2])?,
        })
    }
}

fn main() {
    let v: Version = "1.78.0".parse().unwrap();
    println!("{:?}", v);
}

In Produktions-Code würde der Error-Typ typischerweise ein eigenes enum mit thiserror::Error-derive sein — ein String als Error ist „quick and dirty".

Der Rückweg: Display und to_string

Das Gegenstück zu FromStr ist Display. Wer Display implementiert, bekommt automatisch to_string() mit:

Rust Round-Trip
use std::fmt;

#[derive(Debug)]
struct Version { major: u32, minor: u32, patch: u32 }

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

fn main() {
    let v = Version { major: 1, minor: 78, patch: 0 };
    let s = v.to_string();           // "1.78.0"
    assert_eq!(s, "1.78.0");
}

Idealerweise sind Display und FromStr invers zueinander — parse(&v.to_string()) sollte v ergeben. Das ist eine wichtige API-Eigenschaft für Konfigurations- und Serialisierungs-Typen.

Parse + Display als Round-Trip-Test

Rust Test
#[test]
fn round_trip() {
    let v: Version = "2.34.1".parse().unwrap();
    assert_eq!(v.to_string(), "2.34.1");
}

Wenn das funktioniert, hast du saubere Serialisierung — perfekt für Config-Files, Database-Storage, JSON-Felder.

Spezielle Parse-Varianten

from_str_radix für Hex / Octal / Binary

parse() versteht standardmäßig nur Dezimal. Für andere Basen gibt es die from_str_radix-Methode auf Integer-Typen:

Rust Nicht-Dezimal
let hex = u32::from_str_radix("DEADBEEF", 16).unwrap();
let oct = u32::from_str_radix("777", 8).unwrap();
let bin = u8::from_str_radix("10101100", 2).unwrap();

println!("{hex:#x} {oct} {bin:#b}");

Floats mit Komma als Dezimal-Trenner

parse::<f64>() erwartet Punkt als Dezimal-Trenner — ein deutsches 1,5 schlägt fehl. Workaround:

Rust Komma-zu-Punkt
fn parse_deutsch(input: &str) -> Result<f64, std::num::ParseFloatError> {
    input.replace(',', ".").parse()
}

fn main() {
    let p = parse_deutsch("1,5").unwrap();
    assert_eq!(p, 1.5);
}

Locale-sensible Parsing braucht externe Crates (fluent-number-input, lexical).

Praxis: Parsing im echten Code

CLI-Argument validieren

CLI-Tool akzeptiert einen Port. Er muss eine Zahl sein, in u16 passen und ≥ 1024:

Rust Argument-Parser
use std::num::ParseIntError;

#[derive(Debug)]
enum PortError {
    Parse(ParseIntError),
    Privilegiert(u16),
}

impl std::fmt::Display for PortError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            PortError::Parse(e) => write!(f, "Keine gültige Zahl: {e}"),
            PortError::Privilegiert(p) => write!(f, "Port {p} ist privilegiert"),
        }
    }
}

fn parse_port(s: &str) -> Result<u16, PortError> {
    let p: u16 = s.parse().map_err(PortError::Parse)?;
    if p < 1024 { return Err(PortError::Privilegiert(p)); }
    Ok(p)
}

Konfigurations-Datei zeilenweise parsen

Rust Config-Parser
use std::collections::HashMap;

fn parse_config(text: &str) -> HashMap<String, String> {
    text.lines()
        .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
        .filter_map(|l| {
            let (key, wert) = l.split_once('=')?;
            Some((key.trim().to_owned(), wert.trim().to_owned()))
        })
        .collect()
}

fn main() {
    let raw = "
        # Server-Config
        host = example.com
        port = 8080
    ";
    let cfg = parse_config(raw);
    assert_eq!(cfg.get("port").map(|s| s.as_str()), Some("8080"));
}

split_once ist eine sehr nützliche Methode für „spalte auf ersten Trenner".

Log-Zeile in struktiertes Format

Rust Log-Parser
// Format: 2026-05-18T14:23:01 [INFO] Service-X: alles ok
#[derive(Debug)]
struct LogZeile<'a> {
    zeitstempel: &'a str,
    level: &'a str,
    modul: &'a str,
    text: &'a str,
}

fn parse_log(zeile: &str) -> Option<LogZeile> {
    let (zeit, rest) = zeile.split_once(' ')?;
    let rest = rest.strip_prefix('[')?;
    let (level, rest) = rest.split_once(']')?;
    let rest = rest.trim_start();
    let (modul, text) = rest.split_once(':')?;
    Some(LogZeile {
        zeitstempel: zeit,
        level,
        modul,
        text: text.trim_start(),
    })
}

fn main() {
    let l = "2026-05-18T14:23:01 [INFO] Service-X: alles ok";
    if let Some(p) = parse_log(l) {
        println!("{p:#?}");
    }
}

split_once plus strip_prefix plus trim_start — alle drei sind Borrow-only, also keine einzige Allokation für ein voll-strukturiertes Resultat.

HTTP-Status-Code aus String

Rust HTTP-Status
use std::str::FromStr;

#[derive(Debug, PartialEq)]
struct Status(u16);

#[derive(Debug)]
struct StatusError(String);

impl FromStr for Status {
    type Err = StatusError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let n: u16 = s.parse().map_err(|_| StatusError(format!("'{s}' ist keine Zahl")))?;
        if !(100..600).contains(&n) {
            return Err(StatusError(format!("Status {n} außerhalb 100-599")));
        }
        Ok(Status(n))
    }
}

fn main() {
    let s: Status = "404".parse().unwrap();
    assert_eq!(s, Status(404));
}

Eigene Validierungs-Logik im FromStr macht „kompiliert" zu „semantisch geprüft" — einmal hochgezogen, in der gesamten App verfügbar.

Datei einlesen und parse-Pipeline

Rust Pipeline
use std::fs;

fn lade_zahlen(pfad: &str) -> Result<Vec<i64>, Box<dyn std::error::Error>> {
    let inhalt = fs::read_to_string(pfad)?;
    let zahlen: Result<Vec<i64>, _> = inhalt
        .lines()
        .filter(|l| !l.trim().is_empty())
        .map(|l| l.trim().parse::<i64>())
        .collect();           // Result<Vec<i64>, _>
    Ok(zahlen?)
}

collect() ist hier besonders elegant: wenn der Iterator Result<T, E> liefert, kann er direkt in Result<Vec<T>, E> collected werden — beim ersten Fehler bricht er ab. Klassisches Idiom für „parse alles oder gar nichts".

Häufige Stolperfallen

parse ohne Type-Hint scheitert oft an Inferenz.

"42".parse() allein ist mehrdeutig — der Compiler weiß nicht, ob du i32, u8, f64, IpAddr willst. Lösung: Annotation links (let x: i32 = ...) oder Turbofish ("42".parse::<i32>()).

unwrap in parse ist ein Code-Smell außerhalb Tests.

Außer im Notfall sollte produktiver Code mit ?, unwrap_or, unwrap_or_else oder match arbeiten. Ein parse().unwrap() mit unvertrauenswürdigem Input ist ein wartender Panic.

Float-Parsing mit deutschem Komma scheitert.

"1,5".parse::<f64>() ist ein Fehler. Rusts f64::from_str folgt der C/JSON-Konvention mit Punkt. Wer lokales Komma braucht: replace(',', ".") vor dem parse oder ein Locale-Crate.

Leading Whitespace ist meistens kein Problem, Trailing schon.

" 42".parse::<i32>() gibt einen Err — Whitespace wird nicht automatisch getrimmt. Vor dem Parsen s.trim().parse() aufrufen, wenn User-Input verarbeitet wird.

parse:: akzeptiert -0 aber kein +42 in älteren Rust-Versionen.

Seit Rust 1.34 ist +42 akzeptiert. Bei sehr alten Compilern wäre das ein Err. Heute kein Thema mehr; wer auf Legacy-MSRV setzt, sollte es im Hinterkopf haben.

collect auf Result-Iteratoren bricht beim ersten Fehler ab.

Sehr nützlich für Parse-Pipelines: iter.map(|x| x.parse::<i32>()).collect::<Result<Vec<_>, _>>() liefert entweder einen vollständigen Vec oder den ersten Fehler. Wer Teil-Erfolge will, braucht partition mit eigenem Sammler.

str::FromStr existiert tatsächlich — und ist trivial.

String::from_str("hi") gibt Ok("hi".to_string()). Die Err-Variante ist Infallible. Praktisch in generischen Kontexten, wo du irgendeinen Typ via parse() produzieren willst, der zufällig schon ein String ist.

Beim eigenen FromStr type Err mit Bedacht wählen.

Wer Err = String setzt, hat einen verständlichen Fehler — aber keine strukturierte Behandlung möglich. Besser: ein eigenes enum-Error mit #[derive(Debug)] und impl std::error::Error. Mit dem thiserror-Crate ist das ein 5-Zeilen-Hack.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Strings & Text

Zur Übersicht