Safeguard
Security Guides•

How do you handle a panic in a Rust web handler?

tower-http's CatchPanicLayer turns handler panics into 500 responses, but prefer Result for errors and check how your panic settings affect it.

Safeguard Security Team
4 min read

Prefer returning a Result from handlers and mapping errors to a status code. As a safety net, add tower-http's CatchPanicLayer, which catches panics and converts them into 500 Internal Server Error responses. It is a backstop, not an error handling strategy, and its docs say using panics for error handling is not recommended.

What the layer does

The tower-http documentation says CatchPanic "catches panics and converts them into 500 Internal Server responses". The default response comes from DefaultResponseForPanic, and CatchPanicLayer::custom lets you build your own response. The docs also state plainly: "using panics for error handling is not recommended. Prefer instead to use Result whenever possible."

The page does not say how the layer behaves when a program is built with panic set to abort. Because it turns a panic into a response, it logically depends on unwinding, but treat that as something to verify in your own build before you rely on it.

Why a panic in a handler is a security matter

Most panics are ordinary bugs: an unwrap on bad input, an index out of range, an integer conversion. Whenever user input can reach one of these, an attacker can trigger it on demand. Depending on runtime and settings, that can drop one request, kill a worker task or take down the process.

Panics also tend to print messages and backtraces. Keep those in logs, not in responses.

Patterns that reduce panics

Instead ofPrefer
unwrap or expect on user inputReturn an error with the question mark operator
Direct indexing such as items[i]get(i), then handle None
Unchecked arithmetic on sizeschecked_add and checked_mul, then handle None
Silent as casts for sizestry_from, then handle the error
Unbounded recursion on nested inputDepth limits in parsers

The post on integer overflow in debug and release builds explains why arithmetic needs explicit handling.

Example

use tower_http::catch_panic::CatchPanicLayer;

let app = Router::new()
    .route("/api/items/:id", get(get_item))
    .layer(CatchPanicLayer::new());

If you need a JSON body and a request identifier in the response, use the custom constructor and keep the panic message out of it.

What to log

A useful panic record has the route, a request identifier, the build version and the panic location, and nothing from the request body. Panic messages can contain the value that caused the problem, such as a slice of user input, so treat the log as sensitive and set a short retention period. If you forward logs to a third party service, scrub the message field.

Steps

  1. Search handlers for unwrap, expect, direct indexing and as casts on request data.
  2. Replace them with fallible code that returns a proper error type.
  3. Add CatchPanicLayer as the outermost layer.
  4. Log panics with route and request identifier, and alert on them.
  5. Confirm in a test that a forced panic returns 500 in your actual release build profile.
  6. Fuzz any parser that handles untrusted input.

Quick check

Does CatchPanicLayer catch panics in tasks I spawn?

The layer wraps the service call. A task you spawn separately is outside it, so handle errors and panics there yourself.

Should the 500 response include the panic message?

No. Return a generic message and log the details, as OWASP error handling guidance recommends.

Safeguard's dependency scanning and SBOM generation read Rust lock files, so the crates you ship are inventoried and checked against advisories, and the free scan is at gold.safeguard.sh/scan.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.