# När Rust får panik gör C++ ett undantag

21 augusti 2026 | https://www.ribomation.se/blog/2026/nar-rust-far-panik-gor-c++-ett-undantag/

Om du frågar någon som inte är en rutinerad Rust-programmerare vad en `panic!` är, är svaret nästan alltid "_Rusts motsvarighet till en C++-exception_". Rimlig gissning kanske — båda avbryter det normala flödet, båda avvecklar (rullar tillbaka) stacken (eng. _unwind_), båda kan krascha programmet om inget fångar dem. Jag tänkte utgå från just den uppfattningen och se hur långt det resonemanget håller.

Fyra jämförelser, i tur och ordning:

1.  `panic!` kontra `throw` — samma sorts krasch
2.  `Drop` respektive destruktor — städning under stackavveckling
3.  En panic i en tråd (thread) stannar i tråden
4.  `panic` ligger närmare `terminate` än `throw`

...och på vägen dit landar vi i att uppfattningen ovan var fel på ett intressant sätt, inte bara ett pedantiskt dito.

* * *

1\. panic! kontra throw — samma sorts krasch
--------------------------------------------

Börjar vi där uppfattningen stämmer bäst. En funktion som vägrar fortsätta på grund av felaktig indata, och inget som fångar upp det:

```cpp
int parse_positive(int n) {
    if (n < 0) {
        throw std::invalid_argument("expected a non-negative number, got " + std::to_string(n));
    }
    return n;
}

int main() {
    std::cout << "parsed: " << parse_positive(5) << std::endl;
    std::cout << "parsed: " << parse_positive(-3) << std::endl;
    std::cout << "unreachable" << std::endl;
}
```

```console
$ g++ -std=c++23 -Wall -Wextra parse.cxx -o parse-c++ && ./parse-c++
parsed: 5
terminate called after throwing an instance of 'std::invalid_argument'
  what():  expected a non-negative number, got -3
```

Processen avslutas med exitkod 134 (SIGABRT). `std::invalid_argument` propagerar ut ur `main` ofångad, `std::terminate()` kallas, programmet terminerar abnormalt (eng. _abort_).

```rust
fn parse_positive(n: i32) -> i32 {
    if n < 0 {
        panic!("expected a non-negative number, got {n}");
    }
    n
}

fn main() {
    println!("parsed: {}", parse_positive(5));
    println!("parsed: {}", parse_positive(-3));
    println!("unreachable");
}
```

```console
$ rustc --edition 2024 parse.rs -o parse-rust && ./parse-rust
parsed: 5

thread 'main' (19873) panicked at parse.rs:3:9:
expected a non-negative number, got -3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

Exitkod 101. Precis samma form: flödet avbryts, processen dör med ett felmeddelande som pekar rakt på källan. Hittills stämmer bilden av `panic!` som "Rusts `throw`" perfekt.

2\. Drop respektive destruktor — städning under stackavveckling
---------------------------------------------------------------

Nästa fråga: om något faktiskt fångar felet, städas resurserna upp på vägen ut? En `Trace`\-typ som loggar när den skapas och när den städas, två anrop djupt, där det innersta anropet kastar/panikar och något längst upp fångar det.

```cpp
struct Trace {
    std::string name;
    explicit Trace(std::string n) : name(std::move(n)) {
        std::cout << "  enter " << name << std::endl;
    }
    ~Trace() {
        std::cout << "  exit " << name << std::endl;
    }
};

void inner() {
    auto t = Trace("inner");
    throw std::runtime_error("something went wrong in inner()");
}

void outer() {
    auto t = Trace("outer");
    inner();
}

int main() {
    try {
        outer();
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << std::endl;
    }
    std::cout << "still running" << std::endl;
}
```

```console
$ g++ -std=c++23 -Wall -Wextra unwind.cxx -o unwind-c++ && ./unwind-c++
  enter outer
  enter inner
  exit inner
  exit outer
caught: something went wrong in inner()
still running
```

Destruktorerna körs i omvänd ordning mot konstruktionen medan stacken avvecklas, sen körs `catch`\-blocket, sen fortsätter programmet normalt.

```rust
struct Trace {
    name: &'static str,
}
impl Trace {
    fn new(name: &'static str) -> Self {
        println!("  enter {name}");
        Trace { name }
    }
}
impl Drop for Trace {
    fn drop(&mut self) {
        println!("  exit {}", self.name);
    }
}

fn inner() {
    let _t = Trace::new("inner");
    let reason = "something went wrong in inner()";
    panic!("{reason}");
}

fn outer() {
    let _t = Trace::new("outer");
    inner();
}

fn main() {
    let result = panic::catch_unwind(|| outer());
    match result {
        Ok(()) => println!("no panic"),
        Err(payload) => {
            let reason: Option<&String> = payload.downcast_ref();
            match reason {
                Some(reason) => println!("caught: {reason}"),
                None => println!("caught: unknown panic"),
            }
        }
    }
    println!("still running");
}
```

```console
$ rustc --edition 2024 unwind.rs -o unwind-rust && ./unwind-rust
  enter outer
  enter inner

thread 'main' (28108) panicked at unwind.rs:23:5:
something went wrong in inner()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  exit inner
  exit outer
caught: something went wrong in inner()
still running
```

Samma städordning via `Drop`, samma fortsättning efteråt via `std::panic::catch_unwind`. Ett kvarvarande gissningsbrott, om man ska vara petig: Rust skriver panic-diagnostiken till stderr oavsett om något faktiskt fångar den. En fångad C++-exception ger inget eget utskrift alls om inte `catch`\-blocket loggar den själv. Men i stort: hittills håller gissningen.

Några rader kod ovan förtjänar en snabb förklaring. `std::panic::catch_unwind` tar en closure — Rusts motsvarighet till en C++ lambda expression — kör den, och om en panic inträffar under körningen fångas avvecklingen upp istället för att fortsätta propagera. Här är closuren `|| outer()`: inga parametrar, kroppen anropar bara `outer` och returnerar dess resultat. (`outer` hade faktiskt dugt som argument på egen hand också, eftersom Rust konverterar en funktion till en closure automatiskt — men det är inte uppenbart om man inte redan vet det, så jag skriver en explicit closure.)

Returtypen är `Result<T, Box<dyn Any + Send>>` — `Ok(t)` om inget panikade, annars `Err(payload)` med panic-värdet typraderat i en `Box<dyn Any>`. För att få tillbaka något utskrivbart måste man fråga payloaden "är du faktiskt den här konkreta typen?" — det är vad `downcast_ref` gör. Jag har låtit `reason`\-variabelns typannotering, `Option<&String>`, styra vilken typ det frågas efter, istället för att skriva ut typen i själva anropet — samma resultat, men frågan om `panic!`\-payloadens typ hör egentligen till ett separat ämne (turbofish-syntaxen `::<T>`) som får en egen artikel senare i höst. Anledningen till att jag skriver `let reason = "..."; panic!("{reason}");` istället för att bara skriva `panic!("...")` direkt: en `panic!` med ett rent stränglitteral ger en payload av typen `&str`, medan en `panic!` som interpolerar något (som `{reason}` här, eller `{n}` i avsnitt 1) går via samma formateringsmaskineri som `format!`/`println!` och ger en `String` istället. Jag föredrar att downcasta till `String` här, vilket innebär att meddelandet byggs med interpolation och liknar övriga utskrifter i artikeln.

3\. En panic i en tråd stannar i tråden
---------------------------------------

Här börjar det knaka. Tre arbetartrådar, en av dem misslyckas halvvägs.

```cpp
void worker(int id) {
    std::this_thread::sleep_for((3 - id) * 100ms);
    if (id == 1) {
        throw std::runtime_error("worker 1 hit a bad value");
    }
    std::cout << "worker " << id << " done" << std::endl;
}

int main() {
    auto workers = std::vector<std::thread>{};
    for (int id = 0; id < 3; ++id) {
        workers.emplace_back(worker, id);
    }
    for (auto& t : workers) {
        t.join();
    }
    std::cout << "main: all workers joined" << std::endl;
}
```

```console
$ g++ -std=c++23 -Wall -Wextra threads.cxx -o threads-c++ -pthread && ./threads-c++
worker 2 done
terminate called after throwing an instance of 'std::runtime_error'
  what():  worker 1 hit a bad value
```

Exitkod 134. `worker 0` (medvetet fördröjd att bli klar sist) hinner aldrig skriva ut sin rad, `main` hinner aldrig förbi det första `join()`\-anropet — exceptionen som läcker ut ur trådens startfunktion kallar `std::terminate()`, och det river ner **hela processen**, övriga trådar inräknat.

```rust
fn worker(id: i32) {
    thread::sleep(Duration::from_millis((3 - id as u64) * 100));
    if id == 1 {
        let reason = "worker 1 hit a bad value";
        panic!("{reason}");
    }
    println!("worker {id} done");
}

fn main() {
    let workers: Vec<_> = (0..3).map(|id| thread::spawn(move || worker(id))).collect();

    for (id, handle) in workers.into_iter().enumerate() {
        match handle.join() {
            Ok(()) => println!("main: worker {id} joined normally"),
            Err(payload) => {
                let reason: Option<&String> = payload.downcast_ref();
                match reason {
                    Some(reason) => println!("main: worker {id} panicked: {reason}"),
                    None => println!("main: worker {id} panicked: unknown panic"),
                }
            }
        }
    }
    println!("main: all workers joined");
}
```

```console
$ rustc --edition 2024 threads.rs -o threads-rust && ./threads-rust
worker 2 done

thread '' (29966) panicked at threads.rs:8:9:
worker 1 hit a bad value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
worker 0 done
main: worker 0 joined normally
main: worker 1 panicked: worker 1 hit a bad value
main: worker 2 joined normally
main: all workers joined
```

Exitkod 0. `worker 0` och `worker 2` blir klara. `JoinHandle::join()` för den panikande tråden ger `Err` med panic-payloaden istället för `Ok(())` — felet stannar i tråden det inträffade i, och `main` bestämmer själv vad som ska hända med den informationen. Det här är ingen detalj. En ofångad exception i en `std::thread` är, praktiskt sett, en väg att terminera hela programmet från en enda arbetartråd. Motsvarigheten i Rust terminerar bara en enstaka tråd.

4\. panic ligger närmare terminate än throw
-------------------------------------------

Föregående avsnitts C++-krasch och det här avsnittets undersökning hänger ihop: vad händer i C++ när **ingen** fångar exceptionen, och vad händer i Rust vid samma situation? Samma `Trace`/`inner`/`outer`\-program som i avsnitt 2, men utan `try`/`catch` respektive `catch_unwind`.

```cpp
int main() {
    outer();   // nothing catches this
    std::cout << "unreachable" << std::endl;
}
```

```console
$ g++ -std=c++23 -Wall -Wextra uncaught.cxx -o uncaught-c++ && ./uncaught-c++
  enter outer
  enter inner
terminate called after throwing an instance of 'std::runtime_error'
  what():  something went wrong in inner()
```

Exitkod 134. Inga `exit`\-rader — GCC kallar `std::terminate()` utan att avveckla stacken, så Trace-objektens **destruktorer körs aldrig**.

```rust
fn main() {
    outer(); // nothing catches this
    println!("unreachable");
}
```

```console
$ rustc --edition 2024 uncaught.rs -o uncaught-rust && ./uncaught-rust
  enter outer
  enter inner

thread 'main' (28206) panicked at uncaught.rs:21:5:
something went wrong in inner()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  exit inner
  exit outer
```

Exitkod 101, inte abortad. Trots att inget fångar den avvecklar Rusts standardstrategi ändå och kör `Drop` på vägen ut — städningen sker oavsett om någon lyssnar. Kompilerar man samma fil med `-C panic=abort` istället:

```console
$ rustc --edition 2024 -C panic=abort uncaught.rs -o uncaught-abort-rust && ./uncaught-abort-rust
  enter outer
  enter inner

thread 'main' (28234) panicked at uncaught.rs:21:5:
something went wrong in inner()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

Exitkod 134 — samma som C++. Inga `exit`\-rader här heller: med `panic=abort` sker ingen stackavveckling alls, `Drop` körs aldrig, processen bara abortar. Det är det här läget som faktiskt matchar C++:s beteende för en ofångad exception. Rusts standardläge, `panic=unwind`, gör mer jobb än C++:s `std::terminate()` någonsin gör.

Och det är där gissningen från början av artikeln faller isär på riktigt. `panic!` är inte designad för att bli fångad och hanterad som ett normalt kontrollflöde — `catch_unwind` finns, men dokumentationen avråder uttryckligen från att använda den så, av samma skäl man inte skulle bygga applikationslogik ovanpå `std::terminate()` i C++. En panic betyder "det här är ett programmeringsfel, en bruten invariant, något som aldrig skulle hänt" — närmare ett `assert()`\-fel eller ett ofångat undantag i C++ än ett avsiktligt kastat och fångat `throw`.

* * *

Den riktiga motsvarigheten: Result<T, E> och std::expected<T, E>
----------------------------------------------------------------

Om `panic!` egentligen svarar mot "programmet är trasigt, avsluta", vad svarar då mot C++:s vanliga användning av exceptions — fel som anroparen förväntas hantera, inte krascha på? Samma uppgift, båda språken: tolka en sträng till ett icke-negativt heltal, dubbla det, rapportera ett felmeddelande annars.

```cpp
auto parse_number(const std::string& s) -> std::expected<int, std::string> {
    auto value = 0;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    if (ec != std::errc{} || ptr != s.data() + s.size()) {
        return std::unexpected("not a number: " + s);
    }
    if (value < 0) {
        return std::unexpected("must be non-negative: " + s);
    }
    return value;
}

auto compute(const std::string& s) -> std::expected<int, std::string> {
    return parse_number(s).and_then([](int n) -> std::expected<int, std::string> {
        return n * 2;
    });
}

int main() {
    for (const auto& input : {"21", "-5", "abc"}) {
        auto result = compute(input);
        if (result.has_value()) {
            std::cout << std::quoted(input) << " -> " << result.value() << std::endl;
        } else {
            std::cout << std::quoted(input) << " -> error: " << result.error() << std::endl;
        }
    }
}
```

```console
$ g++ -std=c++23 -Wall -Wextra compute.cxx -o compute-c++ && ./compute-c++
"21" -> 42
"-5" -> error: must be non-negative: -5
"abc" -> error: not a number: abc
```

`std::expected<int, std::string>` (C++23) bär felet i returtypen istället för att kasta det. `.and_then()` kedjar ett steg som kan misslyckas på ett annat, utan ett manuellt `if (!result) return ...` mellan varje steg.

```rust
fn parse_number(s: &str) -> Result<i32, String> {
    let value: i32 = s.parse().map_err(|_| format!("not a number: {s}"))?;
    if value < 0 {
        return Err(format!("must be non-negative: {s}"));
    }
    Ok(value)
}

fn compute(s: &str) -> Result<i32, String> {
    let n = parse_number(s)?;
    Ok(n * 2)
}

fn main() {
    for input in ["21", "-5", "abc"] {
        match compute(input) {
            Ok(n) => println!("{input:?} -> {n}"),
            Err(e) => println!("{input:?} -> error: {e}"),
        }
    }
}
```

```console
$ rustc --edition 2024 compute.rs -o compute-rust && ./compute-rust
"21" -> 42
"-5" -> error: must be non-negative: -5
"abc" -> error: not a number: abc
```

Identisk utskrift — `{input:?}` (Rusts `Debug`\-formatering för `&str`) sätter citattecken runt en textsträng på samma sätt som `std::quoted` gör på C++-sidan. `parse_number(s)?` inuti `compute` gör samma jobb som `.and_then()`\-kedjan: propagera `Err` direkt, fortsätt bara på `Ok`. Ingen panic, ingen `throw`, ingen stackavveckling någonstans i den här filen — felet är bara ett värde, precis som `std::expected` gör det till ett värde istället för ett kontrollflödeshopp.

* * *

Och vad blir kvar av det?
-------------------------

`panic!` **liknar** `throw` på ytan — båda avbryter flödet, båda avvecklar stacken, båda kan krascha programmet ofångat, och avsnitt 1 och 2 visade att likheten håller ganska långt. Men avsnitt 3 och 4 visar var den faller isär: en C++-exception som läcker ut ur en tråd tar hela processen med sig, en Rust-panic tar med sig tråden den hände i. Och `panic=abort` avslöjar vad `panic!` egentligen är designad för — samma sak som `std::terminate()`, ett nödstopp för brutna invarianter, inte ett verktyg för normal felhantering.

Den jämförelsen man faktiskt vill göra för "fel anroparen förväntas hantera" är `Result<T, E>` respektive `std::expected<T, E>` (C++23) — två språk som landat i påfallande lika lösningar på samma problem: gör felet till ett värde i returtypen istället för ett dolt kontrollflöde.

Om något i den här artikeln var värt att ta med sig är det just det: fråga inte "vad är Rusts motsvarighet till en C++-exception", fråga "vad är felet till för" — och svara på den frågan innan du väljer verktyg i endera språket.

Detta var ytterligare ett exempel på vad jag beskrev i min första artikel, nämligen att standardiseringskommittén för C++ aktivt fokuserar på _safety_ och lånar språkelement från Rust.
