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:
panic!kontrathrow— samma sorts kraschDroprespektive destruktor — städning under stackavveckling- En panic i en tråd (thread) stannar i tråden
panicligger närmareterminateänthrow
...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:
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;
}
$ 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).
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");
}
$ 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.
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;
}
$ 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.
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");
}
$ 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.
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;
}
$ 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.
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");
}
$ 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.
int main() {
outer(); // nothing catches this
std::cout << "unreachable" << std::endl;
}
$ 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.
fn main() {
outer(); // nothing catches this
println!("unreachable");
}
$ 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:
$ 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.
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;
}
}
}
$ 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.
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}"),
}
}
}
$ 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.
Länkar
Senaste Artiklarna
-
När Rust får panik gör C++ ett undantag
21 augusti 2026 -
Rust syntax jämfört med C++
14 augusti 2026 -
Fem skäl att välja Rust över C++
10 augusti 2026 -
Apollo 11 och kvinnan bakom koden
12 april 2026 -
Hur gör man en Ralph Loop?
6 april 2026 -
Kan du alla nya AI termer?
22 mars 2026 -
OpenClaw - Det ska vara en hummer i år
15 mars 2026 -
Så här kopplar du SMHI till Claude AI
16 februari 2026