Panic Handlers in no_std
In a standard hosted Rust program, the std library provides the default panic handler that prints the panic message and stack backtrace to stderr before unwinding the thread or aborting the process. In a #![no_std] binary, there is no operating system stderr or default runtime, so the final binary crate must define exactly one function annotated with #[panic_handler] with the signature fn(&core::panic::PanicInfo) -> !.
Panic Triggered in `#![no_std]` Code (`panic!("...")`, `assert!`, slice bounds check)
|
v
`#[panic_handler] fn panic(info: &PanicInfo) -> !`
|
+----------------------------+----------------------------+
| | |
v v v
[ Halt Loop (`loop {}`) ] [ Hardware Reset (`SCB`) ] [ Log to UART/RTT + Breakpoint ]
(`panic-halt` crate) (`panic-reset` crate) (`panic-probe` / `panic-rtt-target`)
Signature and Rules of #[panic_handler]
The function marked with #[panic_handler] must obey three rules:
- It takes a single parameter of type
&core::panic::PanicInfo(which provides.location()and.message()). - Its return type is the never type
!(diverging function): it must never return to its caller. - Exactly one
#[panic_handler]may exist in the entire linked dependency graph of a binary. (Therefore,#![no_std]library crates should never define a#[panic_handler]outside of tests—leave that choice to the final firmware binary!)
// In a `#![no_std]` firmware binary (`#![no_std]` + `#![no_main]`):
//
// use core::panic::PanicInfo;
// use core::sync::atomic::{compiler_fence, Ordering};
//
// #[panic_handler]
// fn panic(_info: &PanicInfo) -> ! {
// loop {
// compiler_fence(Ordering::SeqCst);
// }
// }
Inspecting PanicInfo Location and Message (PanicMessage)
Stabilized in Rust 1.81, PanicInfo::message(&self) -> PanicMessage<'_> implements core::fmt::Display directly on stable Rust! This allows a custom #![no_std] panic handler to format the exact panic message and source location (file:line:column) into a ring buffer, non-volatile flash log, or UART serial register without heap allocation:
use core::fmt::{self, Write};
use core::panic::Location;
pub struct CrashLogBuffer {
bytes: [u8; 128],
len: usize,
}
impl CrashLogBuffer {
pub const fn new() -> Self {
Self {
bytes: [0u8; 128],
len: 0,
}
}
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.bytes[..self.len]).unwrap_or("<invalid utf8>")
}
}
impl Write for CrashLogBuffer {
fn write_str(&mut self, s: &str) -> fmt::Result {
let b = s.as_bytes();
let available = self.bytes.len().saturating_sub(self.len);
let to_copy = b.len().min(available);
self.bytes[self.len..self.len + to_copy].copy_from_slice(&b[..to_copy]);
self.len += to_copy;
Ok(())
}
}
#[track_caller]
pub fn record_diagnostic_snapshot(reason: fmt::Arguments<'_>) -> CrashLogBuffer {
let loc: &Location<'_> = Location::caller();
let mut log = CrashLogBuffer::new();
let _ = write!(
&mut log,
"PANIC at {}:{}:{}: {}",
loc.file(),
loc.line(),
loc.column(),
reason
);
log
}
fn main() {
let sensor_val = 4095;
let crash_log = record_diagnostic_snapshot(format_args!(
"ADC voltage out of bounds ({sensor_val})"
));
println!("Formatted crash log: {}", crash_log.as_str());
}
Formatted crash log: PANIC at src/main.rs:51:21: ADC voltage out of bounds (4095)
Standard Ecosystem Panic Handler Crates for Embedded Rust
Instead of writing a #[panic_handler] from scratch in every firmware project, embedded binaries typically import one of the community panic-handler crates (use panic_probe as _;):
| Crate | Behavior on Panic | Best Used For |
|---|---|---|
panic-halt |
Disables interrupts and enters an infinite loop {} (0 bytes of formatting code!). |
Size-constrained production microcontrollers with a hardware watchdog timer. |
panic-reset |
Immediately triggers a CPU system reset via the architecture reset register. | Failsafe production devices that must reboot immediately on fault. |
panic-probe / defmt-rtt |
Streams the formatted panic message over SWD/JTAG RTT to the host debugger and halts (bkpt). |
Interactive hardware debugging with probe-rs. |
panic-persist |
Writes the panic message to a reserved RAM section that survives a soft reset so the next boot can report it! | Field-deployed IoT devices reporting crash telemetry after reboot. |