How to Use Panic Handlers in Zig for ESP32: A Complete Guide to ESP-IDF Integration
To use panic handlers in Zig for ESP32, override the global panic symbol with a custom implementation that routes messages to the ESP-IDF logging system, then wire it into your root module via pub const panic = idf.esp_panic.panic;.
The kassane/zig-esp-idf-sample repository demonstrates how to override Zig's default @panic behavior for embedded ESP32 development. By implementing custom panic handlers in Zig for ESP32, you can redirect critical error messages to the ESP-IDF logging infrastructure instead of the default stderr, ensuring visibility over UART or RTT during hardware debugging.
Implementing the Custom Panic Handler
The core implementation resides in imports/panic.zig. This file defines a function that matches Zig's expected panic signature while integrating with ESP-IDF's esp_log_write system.
The handler accepts a message, optional stack trace, and return address. It writes a timestamped panic line using sys.esp_log_write, conditionally dumps stack trace addresses, then halts the CPU in an infinite loop:
pub fn panic(msg: []const u8,
stack_trace: ?*@import("std").builtin.StackTrace,
_: ?usize) noreturn {
// Write a timestamped panic line to the ESP‑IDF log
sys.esp_log_write(log.default_level, "PANIC",
"[%lu ms] PANIC: %.*s\n", sys.esp_log_timestamp(),
msg.len, msg.ptr);
// If a stack trace is available, dump each address
if (stack_trace) |st| {
var i: usize = st.index;
if (i > st.instruction_addresses.len) i = st.instruction_addresses.len;
var idx: usize = 0;
while (idx < i) : (idx += 1) {
sys.esp_log_write(log.default_level, "PANIC",
" #%u: 0x%08lx\n", idx,
st.instruction_addresses[idx]);
}
}
// Spin forever – the CPU is effectively halted
while (true) {
asm volatile ("" ::: "memory");
}
}
Wiring the Panic Handler to Your Application
To activate the custom handler, you must expose it as the root-level panic symbol. The repository uses an umbrella module pattern in imports/idf.zig to organize ESP-IDF bindings.
The Umbrella Module Pattern
The imports/idf.zig file aggregates all ESP-IDF imports and re-exports the panic implementation as idf.esp_panic. This centralizes ESP-IDF dependencies and provides a clean namespace for Zig applications.
Root-Level Symbol Export
Each application entry point must re-export the panic function to override Zig's default. This is achieved by declaring pub const panic at the root of your main file:
pub const panic = idf.esp_panic.panic;
This pattern appears in main/examples/wifi-station.zig and main/examples/smartled-rgb.zig. Because the symbol is named exactly panic and is placed in the root namespace of the program, any @panic("error message") emitted by Zig's standard library or user code resolves to this ESP-IDF-specific implementation.
Practical Usage Examples
Wi-Fi Station Example
In main/examples/wifi-station.zig, the panic handler is wired at the top level before any networking logic:
const idf = @import("idf");
pub const panic = idf.esp_panic.panic;
pub fn main() void {
// Wi-Fi initialization code...
const result = someEspIdfCall() catch |err| @panic(@errorName(err));
}
When a Wi-Fi configuration fails, the @panic call invokes the custom handler, writing the error to the ESP-IDF log with a millisecond timestamp before halting.
LED Strip Example
Similarly, main/examples/smartled-rgb.zig demonstrates the same pattern for LED control tasks. The handler captures panics from task creation failures or hardware initialization errors, routing them through the ESP-IDF logging system rather than silently crashing.
Why Override the Default Panic Handler?
Implementing a custom panic handler provides three critical advantages for ESP32 development:
- Visibility: The ESP-IDF logging system routes output to UART, RTT, or other configured sinks, making panic information visible during development without requiring a debugger attachment.
- Consistency: All panic messages share the same format (
[timestamp] PANIC: …) and are tagged with the"PANIC"identifier, which can be filtered in ESP-IDF log viewers likeidf.py monitor. - Debugging: Optional stack-trace dumping helps locate the failure point by printing instruction addresses, aiding post-mortem analysis when a full debugger is unavailable.
Summary
- Implement the panic handler in a dedicated file (e.g.,
imports/panic.zig) using the signaturepub fn panic(msg: []const u8, stack_trace: ?*StackTrace, _: ?usize) noreturn. - Route messages to ESP-IDF using
sys.esp_log_writewithlog.default_leveland the"PANIC"tag. - Halt the CPU with an infinite
while (true)loop containingasm volatile ("" ::: "memory")to prevent optimization. - Re-export the handler in your root module via
pub const panic = idf.esp_panic.panicto override Zig's default behavior. - Reference the pattern in
main/examples/wifi-station.zigandmain/examples/smartled-rgb.zigfor production-ready implementations.
Frequently Asked Questions
How do I enable stack traces in Zig panics for ESP32?
Stack traces are automatically passed to your panic handler via the stack_trace parameter when available. The imports/panic.zig implementation checks if (stack_trace) |st| and iterates through st.instruction_addresses to print each address. Ensure your build mode is not ReleaseSmall or ReleaseFast if you need full debug information.
What happens after the panic handler logs the message?
After writing the panic message and optional stack trace to the ESP-IDF log, the handler enters an infinite while (true) loop with a memory clobber asm volatile statement. This effectively halts the CPU, preventing further execution and allowing you to read the logged error state.
Can I use the standard library panic instead of ESP-IDF logging?
While you can use the standard library's default panic handler, it writes to stderr which may not be visible on ESP32 hardware without specific configuration. The ESP-IDF logging approach in kassane/zig-esp-idf-sample ensures output appears in idf.py monitor and other ESP-IDF tools, making it superior for embedded debugging.
Where should I place the pub const panic declaration?
The declaration must appear in the root namespace of your program (typically main.zig or your entry point file). Zig searches for a public symbol named exactly panic in the root module to determine which function to call when @panic is invoked. Placing it in sub-modules will not override the default handler.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →