Skip to content
rusthacks.dev
Systems

Rust Panics Without Std and Choosing a Panic Handler

In no_std a panic handler is mandatory. Compare panic-halt, panic-abort, panic-probe and a custom handler, and how panic = abort and the core panic machinery fit together.

3 min read
Summarize in AI
Flat illustration for the post “Rust Panics Without Std and Choosing a Panic Handler”, composed around errors.

On a desktop, a panic prints a message and unwinds. On a microcontroller there is nowhere to print and nothing to unwind into, so you decide what a panic means. The language makes you decide: a no_std program does not link without a handler. Everything here applies to Rust 1.90.

The mandatory function

rust
#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

When any panic!, failed assert!, unwrap() on None or out-of-bounds index fires, the compiler routes it here. The function must diverge. There can be only one in the final binary, so you either write one or link a crate that does, not both.

Leaving it out gives:

Text
error: `#[panic_handler]` function required, but not found

Ready made handlers

You choose by importing, which links the crate and registers its handler:

rust
use panic_halt as _;
CrateBehaviorUse it for
panic-haltloops foreverbring up, size critical builds
panic-abortexecutes an abort instruction (nightly on some targets)tiny binaries
panic-probelogs through defmt, triggers a breakpointdevelopment with a debugger
panic-semihostingprints through the debug probe, slowsimple debugging
panic-resetresets the chipunattended devices that should recover
panic-persistsaves the message in RAM that survives resetread the cause after the reboot

Only one may appear in a dependency graph. During development panic-probe with print-defmt is the practical choice (defmt logging). For deployed hardware, panic-reset or panic-persist avoids a frozen device.

Panic profile

Cargo.tomlTOML
[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Embedded targets have no unwinder, so abort is the only mode that works, and it keeps size down by dropping landing pads (shrinking a binary). The consequence: destructors do not run when a panic happens, so a Drop guard that turns off a motor does nothing.

A custom handler that makes the hardware safe

rust
use core::panic::PanicInfo;
use core::sync::atomic::{compiler_fence, Ordering};

const MOTOR_ENABLE: *mut u32 = 0x4002_0014 as *mut u32;

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // outputs first: the device must be safe before anything else happens
    unsafe { core::ptr::write_volatile(MOTOR_ENABLE, 0) };

    if let Some(loc) = info.location() {
        // record the line in a reserved RAM word for the next boot to report
        let _ = (loc.file(), loc.line());
    }

    loop {
        compiler_fence(Ordering::SeqCst);
        cortex_m::asm::wfi();
    }
}

The order is the lesson: make the hardware safe, record what happened, then halt or reset. Do not allocate, format into a heap String, or take locks in a handler; the panic may have come from any of those. Keep it allocation free and short.

Avoiding panics at all

The best handler is the one that never runs. Replace unwrap() with handling, use get() instead of indexing, and make arithmetic explicit (integer overflow). clippy::unwrap_used and clippy::indexing_slicing as workspace lints enforce that (workspace lints). Background on what no_std removes is in no_std Rust.

Also inRust
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.