How to Implement Error Handling for the Quadrotor Simulation in Peng

Peng uses a centralized SimulationError enum in src/lib.rs combined with Rust's Result type and the ? operator to propagate failures from matrix operations, OSQP solvers, and logging initialization through every layer of the quadrotor simulation.

Peng is a minimal quadrotor autonomy framework written in Rust that prioritizes safety and debuggability through rigorous error handling. Implementing robust error handling for the quadrotor simulation in Peng requires understanding its centralized SimulationError enum, the Result type propagation pattern, and idiomatic Rust conversion traits.

Understanding the SimulationError Enum

All error conditions in Peng are represented by a single enum SimulationError defined in src/lib.rs. This enum derives thiserror::Error and Debug, automatically implementing std::error::Error with human-readable messages.

The enum variants cover every failure point in the quadrotor simulation:

Variant Description Source Location
RerunError Failure creating or using rerun.io recording streams src/lib.rs lines 64-66
RerunSpawnError Process spawn problems for rerun tools src/lib.rs lines 68-70
SetLoggerError Logger initialization failure src/lib.rs lines 71-73
NalgebraError Linear algebra operations (matrix inversion) fail src/lib.rs lines 74-76
OSQPError OSQP optimization problem setup or solve failure src/lib.rs lines 77-79
NormalError Random distribution creation fails src/lib.rs lines 80-82
OtherError Miscellaneous errors not fitting above categories src/lib.rs lines 83-84

Propagating Errors with Result Types

Every public entry point that can fail returns Result<T, SimulationError>, allowing the ? operator to propagate errors concisely.

Quadrotor Construction

The Quadrotor::new method in src/lib.rs returns Result<Self, SimulationError> because it must invert the inertia matrix during initialization:

// src/lib.rs lines 41-55
pub fn new(
    time_step: f32,
    mass: f32,
    gravity: f32,
    arm_length: f32,
    inertia: [f32; 9],
) -> Result<Self, SimulationError> {
    // ... validation and matrix inversion that may fail
}

IMU and Sensor Initialization

The Imu struct constructor may return SimulationError::NormalError when initializing Gaussian noise distributions using rand_distr:

// src/lib.rs lines 23-28
pub fn new(
    accelerometer_noise_std: f32,
    gyroscope_noise_std: f32,
) -> Result<Self, SimulationError> {
    // ... distribution creation that may fail
}

Configuration Loading

The Config::from_yaml method in src/config.rs returns a generic Box<dyn Error> that callers convert to SimulationError:

// src/config.rs lines 62-66
pub fn from_yaml(path: &str) -> Result<Self, Box<dyn Error>> {
    let contents = std::fs::read_to_string(path)?;
    let config: Config = serde_yaml::from_str(&contents)?;
    Ok(config)
}

Main Simulation Loop

The entry point in src/main.rs returns Result<(), SimulationError>, allowing any error in the simulation loop to abort execution with a clear message:

// src/main.rs lines 4-5
fn main() -> Result<(), SimulationError> {
    // ... simulation setup and loop using ? operator
}

Implementing Custom Error Handling Patterns

When extending Peng with new error-prone functionality, follow this five-step pattern to maintain consistency:

  1. Identify the failure point (file reads, math operations, third-party calls)
  2. Map the failure to an existing SimulationError variant or create a new variant with #[error("...")]
  3. Use the ? operator to convert Result<T, E> into Result<T, SimulationError> by implementing From<E> for SimulationError
  4. Wrap external calls in helpers that add context when the error type doesn't map cleanly
  5. Log at the call site before bubbling if immediate visibility is needed

Implementing From Traits for External Errors

Convert standard library or crate errors into SimulationError using the From trait:

impl From<std::io::Error> for SimulationError {
    fn from(err: std::io::Error) -> Self {
        SimulationError::OtherError(err.to_string())
    }
}

Wrapping External Calls with Context

When external libraries don't return types compatible with SimulationError, wrap them in helpers:

fn load_mesh(path: &str) -> Result<Mesh, SimulationError> {
    std::fs::read_to_string(path)
        .map_err(|e| SimulationError::OtherError(format!("Failed to read mesh: {}", e)))?
        .parse()
        .map_err(|e| SimulationError::OtherError(format!("Mesh parse error: {}", e)))
}

Logging Before Propagation

For critical simulation steps, log errors before returning them:

if let Err(e) = quad.update_dynamics_with_controls_euler(thrust, &torque) {
    log::error!("Dynamics update failed: {}", e);
    return Err(e);
}

Practical Code Examples for Error Handling in Peng

Loading Custom YAML Configurations

When loading simulation parameters from external files, convert generic errors to SimulationError:

use peng_quad::Config;
use peng_quad::SimulationError;

fn load_custom_cfg(path: &str) -> Result<Config, SimulationError> {
    Config::from_yaml(path).map_err(|e| SimulationError::OtherError(e.to_string()))
}

Adding New Error Variants for External Libraries

Extend SimulationError when integrating new hardware or physics libraries:

// In src/lib.rs
#[derive(thiserror::Error, Debug)]
pub enum SimulationError {
    // … existing variants …
    #[error("External library error: {0}")]
    ExternalLibError(String),
}

// Convert the external library's error type
impl From<external_lib::Error> for SimulationError {
    fn from(err: external_lib::Error) -> Self {
        SimulationError::ExternalLibError(err.to_string())
    }
}

Matrix Inversion with Error Propagation

Handle singular matrix errors during dynamics calculations:

use nalgebra::Matrix3;
use peng_quad::SimulationError;

fn invert_matrix(m: Matrix3<f32>) -> Result<Matrix3<f32>, SimulationError> {
    m.try_inverse()
        .ok_or_else(|| SimulationError::NalgebraError("Matrix not invertible".into()))
}

Simulation Loop Error Handling

Propagate planner and controller errors through the main execution flow:

// Inside the main loop
let (desired_position, desired_velocity, desired_yaw) = planner_manager.update(
    quad.position,
    quad.orientation,
    quad.velocity,
    time,
    &maze.obstacles,
)?;

If any planner returns a SimulationError, the ? operator propagates it to main(), which then aborts with a clear message.

Summary

  • Peng centralizes all errors in the SimulationError enum defined in src/lib.rs, covering everything from matrix operations to OSQP solver failures.
  • Every fallible function returns Result<T, SimulationError>, enabling the ? operator to propagate errors concisely through the quadrotor construction, IMU initialization, and simulation loop.
  • The thiserror crate automates boilerplate, implementing std::error::Error and Display via derive macros while allowing custom error messages for each variant.
  • Extending error handling follows a consistent pattern: implement From<ExternalError> for SimulationError, use ? for propagation, and wrap external calls with context when needed.
  • Testing error paths is straightforward because SimulationError derives Debug and PartialEq, allowing assert!(matches!(...)) in unit tests.

Frequently Asked Questions

What is SimulationError in Peng?

SimulationError is a centralized error enum defined in src/lib.rs that represents every possible failure condition in the quadrotor simulation. It includes variants for linear algebra failures (NalgebraError), OSQP optimization errors (OSQPError), logging initialization issues (SetLoggerError), and miscellaneous external failures (OtherError). The enum derives thiserror::Error, which automatically implements the std::error::Error trait and provides formatted error messages.

How do I add custom error handling for new hardware drivers in Peng?

To add error handling for new hardware or external libraries, first add a variant to the SimulationError enum in src/lib.rs with a descriptive #[error("...")] attribute. Then implement From<ExternalDriverError> for SimulationError to enable automatic conversion. Finally, use the ? operator in your driver methods to propagate errors. If the external library uses complex error types that don't map cleanly, wrap the calls in helper functions that convert errors using map_err().

Why does Peng use Result types instead of panicking?

Peng uses Result<T, SimulationError> instead of panicking because the quadrotor simulation must handle recoverable failures gracefully, such as singular inertia matrices during vehicle construction or OSQP solver failures during trajectory optimization. Returning Result forces callers to explicitly handle error cases, prevents undefined states in the physics simulation, and allows the main loop in src/main.rs to abort cleanly with a descriptive error message rather than crashing the process.

How can I test error conditions in the Peng simulation?

You can test error conditions by leveraging the fact that SimulationError derives Debug and PartialEq via the thiserror crate. In unit tests, use assert!(matches!(result, Err(SimulationError::SpecificVariant))) to verify that functions return the expected error variants. For example, test that Quadrotor::new returns NalgebraError when passed a singular inertia matrix, or that Imu::new returns NormalError when given invalid noise parameters.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →