Skip to content

rust.panic_in_library

Stability High Causes Production Outages

Detects panic!(), todo!(), unimplemented!() in library code.

Libraries should return errors, not panic:

  • Crashes the caller — Panics propagate to whoever uses your library
  • No recovery — Callers can’t handle or retry on panic
  • Trust violation — Users expect libraries to be safe
  • Debugging difficulty — Panic traces are hard to understand

Application code can choose to panic. Libraries should not make that choice.

// ❌ Before (library code)
pub fn parse_config(s: &str) -> Config {
if s.is_empty() {
panic!("empty config string");
}
// ...
}
pub fn calculate(x: i32) -> i32 {
todo!("implement later")
}
// ✅ After
pub fn parse_config(s: &str) -> Result<Config, ConfigError> {
if s.is_empty() {
return Err(ConfigError::Empty);
}
// ...
}
pub fn calculate(x: i32) -> Result<i32, Error> {
Err(Error::NotImplemented("calculate"))
}
  • panic!() in non-test, non-main code
  • todo!() in published code
  • unimplemented!() macros
  • unreachable!() that might be reachable
  • .expect() in libraries (less strict)

Unfault can convert panic patterns to Result returns with appropriate error types when the function signature allows.

// Tests - expected behavior
#[test]
fn test_invalid_input() {
let result = std::panic::catch_unwind(|| {
dangerous_function()
});
assert!(result.is_err());
}
// Logic errors that indicate bugs
fn index(&self, idx: usize) -> &T {
// Panic on out-of-bounds is documented contract
&self.data[idx]
}
// Main function initialization
fn main() {
let config = Config::load().expect("failed to load config");
}
// Use thiserror for library errors
#[derive(Debug, thiserror::Error)]
pub enum MyLibError {
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("not implemented: {0}")]
NotImplemented(&'static str),
}
// Document panic conditions if they exist
/// # Panics
///
/// Panics if `divisor` is zero.
pub fn divide(a: i32, divisor: i32) -> i32 {
a / divisor
}