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.

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
#![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:
error: `#[panic_handler]` function required, but not foundReady made handlers
You choose by importing, which links the crate and registers its handler:
use panic_halt as _;| Crate | Behavior | Use it for |
|---|---|---|
panic-halt | loops forever | bring up, size critical builds |
panic-abort | executes an abort instruction (nightly on some targets) | tiny binaries |
panic-probe | logs through defmt, triggers a breakpoint | development with a debugger |
panic-semihosting | prints through the debug probe, slow | simple debugging |
panic-reset | resets the chip | unattended devices that should recover |
panic-persist | saves the message in RAM that survives reset | read 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
[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
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.


