Back

Panic! at the Arduino

2026-07-20

One of the tough things about embedded Rust development is the lack of useful information when your code panic!s. Heck, you don't even know if your code panicked or if it is just not producing output. I'd say the no_std environment really limits you, but even if you could get a backtrace, where would you put it? Many microcontrollers don't have an exposed terminal that you can go and read when the code crashes.

Fortunately, a panic! doesn't have to be an instant halt for your code; instead, you can write your own function to handle it. This function, called the panic handler, gives you an opportunity to take some action in response to the panic.

Setting It Up

Let's start with a basic example that just blinks an LED and writes to a serial output. This code should work with no panic!s!

#![no_std]
#![no_main] // Consequence of no_std

use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut led = pins.d13.into_output();
    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

    let _ = ufmt::uwriteln!(serial, "Booting up...");

    loop {
        arduino_hal::delay_ms(1_000);
        led.set_high();
        arduino_hal::delay_ms(1_000);
        led.set_low();
    }
}

Run this to confirm you have a blinking LED and get a "Booting up..." message on whatever device you have connected. If you don't have an LED or a serial output, skip that part.

Panicking!

Let's take our nice, working code and introduce a panic!.

#![no_std]
#![no_main] // Consequence of no_std

use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut led = pins.d13.into_output();
    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

    let _ = ufmt::uwriteln!(serial, "Booting up...");

    panic!("The code stops here");

    let _ = ufmt::uwriteln!(serial, "This point won't be reached");

    loop {
        arduino_hal::delay_ms(1_000);
        led.set_high();
        arduino_hal::delay_ms(1_000);
        led.set_low();
    }
}

You should get an "unreachable statement" warning, since the compiler knows the code stops at the panic!. You'll also get a note about the mut on led being unnecessary, since none of the uses in the loop will ever happen. Nice job, rustc!

When you run this, you should only get the "Booting up..." message, but no second message or blinking LED. The code has panicked, and now it's time to handle it.

A Minimal Panic Handler

Replace the panic_halt import with the following code from the Rustonomicon.

use core::panic::PanicInfo;

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

Running the code on the microcontroller should give the same outcomes: only one message and no blinking LED. The panic still happened, and the code still halted. We just replaced the panic_halt crate's halting panic handler with our own.

Getting Output

If we want to do anything in the panic handler, we'll need access to the peripherals from the HAL. While the safe way to get them was with take, we can't do that again in the panic handler, since main already took them. The first take gives you the object, the second gives you None as the object was already taken.

Instead, we'll have to steal them!

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    let dp = unsafe { arduino_hal::Peripherals::steal() };
    let pins = arduino_hal::pins!(dp);
    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

    let _ = ufmt::uwriteln!(serial, "Panicked.");

    loop {}
}

Note that stealing is unsafe, so we need to use an unsafe block. We can be confident that other code won't go and mess with the peripherals, since there's no more code outside our panic handler. We only have to worry about complicated panic handlers and interrupts (you might want to disable interrupts if you're using them).

Once we get the device peripherals, we can just use them to write a panic message. Run this and you should see Panicked. printed at the end, with no further activity from the microcontroller. Now we at least know that a panic happened.

Panic LED

If you don't have a serial port accessible, you can also just use an LED to visually indicate failure. Here's the equivalent option.

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    let dp = unsafe { arduino_hal::Peripherals::steal() };
    let pins = arduino_hal::pins!(dp);

    let mut led = pins.d13.into_output();
    led.set_high();

    loop {}
}

As long as you don't use this LED for anything else, you can use it to tell the difference between a panic and a logic error when you see your code stop working.

Getting More Info

So far, we've been ignoring the info we get about the panic and just indicating that a panic occurred. If we want to know more about the panic, we can use the PanicInfo passed as the argument. This struct has two things: a message about the panic, and (optionally) the location in the code.

Here's how you can get the location of your panic in an embedded situation.

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    let dp = unsafe { arduino_hal::Peripherals::steal() };
    let pins = arduino_hal::pins!(dp);

    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

    if let Some(location) = info.location() {
        let _ = ufmt::uwriteln!(serial, "Panic at {}:{}", location.file(), location.line());
    } else {
        let _ = ufmt::uwriteln!(serial, "Panic at unknown");
    }

    loop {}
}

If you run this, it should give the exact line of the panic! in the main above, which would be src/main.rs, line 33 (depending on how you wrote your file).

There's also a message in a PanicInfo, but it only implements Display, not the uDisplay of ufmt. Some messages can be converted to &str for use with ufmt.