# no-panic makes the compiler prove a function cannot panic, and the proof happens at link time

> A Rust attribute macro that turns a panic into a link error naming the offending function. The catch is that the check only runs when the linker runs, and it is useless in a panic = abort build, which makes it a release profile tool rather than a general safety net.

**dtolnay/no-panic** — Attribute macro to require that the compiler prove a function can't ever panic

- Repository: https://github.com/dtolnay/no-panic
- Stars: 1,222 · Forks: 21
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/dtolnay-no-panic

## The attribute turns a panic into a link error

no-panic is a Rust attribute macro that requires the compiler to prove that a function can never panic. The mechanics are unusual, and they are the whole design.

Add the dependency, which is a proc macro crate.

```toml
[dependencies]
no-panic = "0.1"
```

Then mark the function.

```rust
use no_panic::no_panic;

#[no_panic]
fn demo(s: &str) -> &str {
    &s[1..]
}

fn main() {
    println!("{}", demo("input string"));
}
```

Slicing a string at byte one panics on a multi byte character, and if the compiler cannot prove the function is panic free the program fails to compile with a linker error that identifies the function.

The README is honest about the quality of that error. It says the error is not stellar, but points out the part that matters, the `ERROR[no-panic]` line at the end with the name of the offending function. That is a fair description of a diagnostic that arrives as a wall of linker output, and the useful signal is genuinely buried in it.

The mechanism is a link time one. The macro generates code that references an undefined symbol when the function's compiled form still contains a panic path, and the linker fails when it cannot resolve it. So the check is not a lint and not a runtime assertion. It is a proof obligation discharged by the toolchain, and the price is paid at the last step of the build.

## The caveat that decides whether the tool applies to you

There are four documented caveats, and one of them rules the tool out for a large class of projects.

The first is that the attribute is useless in code built with `panic = "abort"`. Panics must be built with `panic = "unwind"`, which is the default, for anything to be detected. The README makes the sensible point that you can still ship as a `panic = "abort"` build after confirming the absence of panics, so the tool is for verification, not for deployment.

The second is the one that surprises people. Panic detection happens at link time across the entire dependency graph, so any Cargo command that does not invoke a linker will not trigger it. That explicitly includes `cargo build` of library crates and `cargo check` of binary and library crates. In other words, the check you get in a fast inner loop is not the check you get in a release build, and a function that is fine today can start failing to link tomorrow without any source change, purely because a dependency changed.

The third is that const functions are not supported and the attribute fails to compile on a `const fn`.

The fourth is optimization related, and it interacts with the second. Functions that need some optimization to be proven panic free may stop compiling in debug mode after being marked. The README's own advice for that case is to make no-panic an optional dependency enabled only in release builds, or to raise the debug optimization level.

```toml
[profile.dev]
opt-level = 1
```

That caveat is the most important line in the README for anyone planning to adopt this, because it means the attribute is not a stable annotation on a function. Its meaning depends on the build profile.

## Thin LTO, then fat LTO, and the escape hatch you should avoid

If the code you need to prove calls non-generic, non-inline functions in another crate, the linker may not be able to deduce that those functions do not panic. The README's escalation path is a ladder of link time optimisation settings, and it is worth reading as a debugging guide rather than as configuration advice.

```toml
[profile.release]
lto = "thin"
```

If thin LTO is not enough, the next step is fat LTO with a single codegen unit.

```toml
[profile.release]
lto = "fat"
codegen-units = 1
```

Both of these slow the build down, and the second one does so significantly. That is the real cost of this tool: proving panic freedom across crate boundaries requires the compiler to see the whole program at once, and the compiler sees the whole program at once only when it is not compiling it in parallel.

The README also names the shortcut, and it is the one to be careful about. If you want no-panic to just assume that a called function does not panic and give you undefined behaviour at runtime if it does, the suggestion is to wrap that call in an `unsafe extern "C"` wrapper. That is a documented way to silence the proof obligation by moving the responsibility to the programmer. It is legitimate for a foreign function boundary where unwinding is not allowed anyway, and it is a way to defeat the entire purpose of the tool if used anywhere else.

The project credits the technique to Kixunil's `dont_panic` crate, which offers other ways to require the absence of panics, and points at it for anyone who finds this approach too limited.

## A proc macro crate with a no_std sibling workspace

The package metadata describes the shape of the implementation. It is a proc macro crate, with `proc-macro2`, `quote` and `syn` with the full feature set as its dependencies, which is the standard toolchain for a Rust attribute macro. The edition is 2021 and the minimum supported Rust version is 1.71.

The categories are listed as `no-std` and `no-std::no-alloc`, which is a meaningful detail. A crate that can be used in a no_std, no allocation environment is a requirement for the code it is meant to protect, because the panic handler and the allocator shim are exactly the places where you cannot afford an allocation.

The workspace contains two more members beyond the main crate, `noexcept` and `noexcept-impl`, with a `build.rs` at the root and a `noexcept/` directory at the top level of the repository. The `noexcept` sibling is a separate macro crate in the same workspace, shipped alongside the `#[no_panic]` attribute discussed here.

The tests directory exists alongside it, and the dev dependencies tell you how the project is tested. `trybuild` with the diff feature is the standard way to test compiler diagnostics, which is what this crate produces, and `rustversion` allows tests to be gated on the Rust version in use, which matters for a tool whose behaviour depends on optimizer output.

The documentation build is configured for a single target, `x86_64-unknown-linux-gnu`, and the rustdoc arguments request link to definition and macro expansion pages, which are the two things a reader of a proc macro needs to see.

## Where no-panic earns its place, and where it does not

The tool is not an alternative to careful Rust. It is a proof obligation you attach to a small number of functions where unwinding is not allowed.

The natural home for it is a panic handler, an allocator implementation, or any code that runs inside a `no_std` environment where a second panic would be an abort. It is also the right tool for a function called from C, where unwinding across the boundary is undefined behaviour, since the tool forces you to find the panic path before you ship rather than after.

What it is not is a way to make a codebase panic free. The detection is a link time analysis of the whole dependency graph, so a marked function whose callees are generic and inlined into a dependency is at the mercy of that dependency's optimisation choices. And since the check does not run in `cargo check` or in a library build, a green CI pipeline that only type checks will not tell you anything.

The MIT or Apache-2.0 dual licence is the standard Rust choice and the README states it explicitly, with the note that contributions are dual licensed as well. The project is not archived, the last push was on 2026-08-03, and the release history shows 0.1.37 on 2026-07-18, 0.1.36 on 2026-01-11 and 0.1.35 on 2025-03-10. The version has been at 0.1 for its whole life, which is honest for a tool whose behaviour depends on compiler internals, since a 1.0 would promise stability of something that is inherently tied to the toolchain's optimizer.

## Conclusion

Adopt no-panic for a small set of functions that must never unwind, such as the body of a panic handler, an allocator shim, or code called from a foreign function boundary where unwinding is undefined behaviour. Do not adopt it as a general lint for your codebase, because the check is a link time analysis over the whole dependency graph, so it only fires on a full build of a binary and a function that fails to prove simply fails to link. Verify first that your build profile uses panic = unwind, that you have a release build with optimization enabled, and that the functions you mark are ones whose callers can cope with a compile failure rather than a runtime error.

## FAQ

### What does the no-panic attribute macro do in Rust?

It requires the compiler to prove that the annotated function can never panic. If the function can panic, or the compiler cannot prove it cannot, the build fails with a linker error containing an ERROR[no-panic] line that names the offending function.

### Why does cargo check not report panics with no-panic?

Panic detection happens at link time across the whole dependency graph, so any Cargo command that does not invoke a linker misses it. That includes cargo build of library crates and cargo check of both binary and library crates.

### Can I use no-panic in a panic = abort build?

No. The attribute is useless with panic = abort because there are no unwinds to detect. You must build with panic = unwind, which is the default, and can still ship an abort build after the absence of panics is confirmed.

### What should I change when a no-panic function needs optimization to compile?

Make no-panic an optional dependency enabled only in release builds, or raise debug optimization with a profile.dev section setting opt-level = 1. Across crate boundaries you may also need thin LTO, and then fat LTO with codegen-units = 1.

### Which Rust version and licence does no-panic require?

The crate declares edition 2021, a minimum supported Rust version of 1.71, and the MIT or Apache-2.0 dual licence. It is a proc macro crate, so it depends on proc-macro2, quote and syn.

## Sources

- [dtolnay/no-panic on GitHub](https://github.com/dtolnay/no-panic)
- [Issues](https://github.com/dtolnay/no-panic/issues)
- [License: Apache-2.0](https://github.com/dtolnay/no-panic/blob/master/LICENSE)
- [README](https://github.com/dtolnay/no-panic/blob/master/README.md)
- [Releases](https://github.com/dtolnay/no-panic/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dtolnay-no-panic
