# How to Implement Error Handling for the Quadrotor Simulation in Peng

> Learn how to implement quadrotor simulation error handling in Peng. Discover how Rust's Result and the ? operator manage failures effectively across all simulation layers.

- Repository: [Yang Zhou/peng](https://github.com/makeecat/peng)
- Tags: how-to-guide
- Published: 2026-03-06

---

**Peng uses a centralized `SimulationError` enum in [`src/lib.rs`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/src/lib.rs) lines 64-66 |
| `RerunSpawnError` | Process spawn problems for rerun tools | [`src/lib.rs`](https://github.com/makeecat/peng/blob/main/src/lib.rs) lines 68-70 |
| `SetLoggerError` | Logger initialization failure | [`src/lib.rs`](https://github.com/makeecat/peng/blob/main/src/lib.rs) lines 71-73 |
| `NalgebraError` | Linear algebra operations (matrix inversion) fail | [`src/lib.rs`](https://github.com/makeecat/peng/blob/main/src/lib.rs) lines 74-76 |
| `OSQPError` | OSQP optimization problem setup or solve failure | [`src/lib.rs`](https://github.com/makeecat/peng/blob/main/src/lib.rs) lines 77-79 |
| `NormalError` | Random distribution creation fails | [`src/lib.rs`](https://github.com/makeecat/peng/blob/main/src/lib.rs) lines 80-82 |
| `OtherError` | Miscellaneous errors not fitting above categories | [`src/lib.rs`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/src/lib.rs) returns `Result<Self, SimulationError>` because it must invert the inertia matrix during initialization:

```rust
// 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`:

```rust
// 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`](https://github.com/makeecat/peng/blob/main/src/config.rs) returns a generic `Box<dyn Error>` that callers convert to `SimulationError`:

```rust
// 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`](https://github.com/makeecat/peng/blob/main/src/main.rs) returns `Result<(), SimulationError>`, allowing any error in the simulation loop to abort execution with a clear message:

```rust
// 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:

```rust
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:

```rust
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:

```rust
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`:

```rust
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:

```rust
// 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:

```rust
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:

```rust
// 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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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.