Home Wiki Programming & Logic Error Handling in Rust: Building Industrial Software That Never Crashes
Programming & Logic

Error Handling in Rust: Building Industrial Software That Never Crashes

Why Rust Has No Exceptions

Many languages use try/catch exceptions. Rust deliberately avoids them because exceptions create invisible control flow -- dangerous in industrial software where a missed error can shut down a production line or damage equipment.

Instead, Rust uses two enum types from the standard library:

  • Option<T> -- for values that might not exist
  • Result<T, E> -- for operations that can succeed or fail

Both force the programmer to handle every case explicitly at compile time.

Option: Values That Might Not Exist

Option<T> has two variants: Some(value) or None. There is no null in Rust.

struct SensorArray {
    sensors: Vec<f64>, // temperature readings
}

impl SensorArray {
    // Returns None if no sensors exist
    fn highest_reading(&self) -> Option<f64> {
        if self.sensors.is_empty() {
            None
        } else {
            let max = self.sensors.iter().cloned().fold(f64::MIN, f64::max);
            Some(max)
        }
    }
}

fn main() {
    let array = SensorArray { sensors: vec![72.1, 85.4, 63.9] };

    // You must handle both cases
    match array.highest_reading() {
        Some(temp) => println!("Highest: {temp}°C"),
        None => println!("No sensors connected"),
    }

    // Or use if-let for when you only care about Some
    if let Some(temp) = array.highest_reading() {
        println!("Peak temperature: {temp}°C");
    }
}

Result<T, E>: Operations That Can Fail

Result<T, E> has Ok(value) for success and Err(error) for failure. Use it for any operation that can go wrong: file I/O, network calls, sensor reads.

use std::fs;

fn read_machine_config(path: &str) -> Result<String, std::io::Error> {
    fs::read_to_string(path)
}

fn main() {
    match read_machine_config("/etc/machine/config.toml") {
        Ok(contents) => println!("Config loaded: {} bytes", contents.len()),
        Err(e) => println!("Failed to load config: {e}"),
    }
}

The ? Operator: Elegant Error Propagation

The ? operator returns early with the error if the result is Err, or unwraps the Ok value. It eliminates deeply nested match blocks.

use std::fs;
use std::io;

struct MachineConfig {
    name: String,
    max_rpm: u32,
}

fn load_config(path: &str) -> Result<MachineConfig, io::Error> {
    let contents = fs::read_to_string(path)?; // returns Err early if it fails
    let lines: Vec<&str> = contents.lines().collect();

    let name = lines.first()
        .ok_or(io::Error::new(io::ErrorKind::InvalidData, "missing name"))?;

    let rpm: u32 = lines.get(1)
        .ok_or(io::Error::new(io::ErrorKind::InvalidData, "missing rpm"))?
        .trim()
        .parse()
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid rpm"))?;

    Ok(MachineConfig {
        name: name.to_string(),
        max_rpm: rpm,
    })
}

Each ? replaces a multi-line match, keeping the code flat and readable.

Custom Error Types with enum

For industrial systems, define your own error types to capture domain-specific failures.

enum MachineError {
    SensorTimeout { sensor_id: u32 },
    OverTemperature(f64),
    CalibrationFailed(String),
    CommunicationLost,
}

impl std::fmt::Display for MachineError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MachineError::SensorTimeout { sensor_id } =>
                write!(f, "Sensor #{sensor_id} timed out"),
            MachineError::OverTemperature(temp) =>
                write!(f, "Temperature {temp}°C exceeds limit"),
            MachineError::CalibrationFailed(msg) =>
                write!(f, "Calibration failed: {msg}"),
            MachineError::CommunicationLost =>
                write!(f, "Communication with PLC lost"),
        }
    }
}

fn check_sensor(id: u32, temp: f64) -> Result<f64, MachineError> {
    if temp > 150.0 {
        Err(MachineError::OverTemperature(temp))
    } else {
        Ok(temp)
    }
}

Converting Between Error Types: The From Trait

When a function can produce different error types, implement From to allow automatic conversion with the ? operator.

use std::io;
use std::num::ParseIntError;

enum ConfigError {
    Io(io::Error),
    Parse(ParseIntError),
    Missing(String),
}

// Automatic conversion from io::Error
impl From<io::Error> for ConfigError {
    fn from(e: io::Error) -> Self {
        ConfigError::Io(e)
    }
}

// Automatic conversion from ParseIntError
impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::Parse(e)
    }
}

fn load_sensor_count(path: &str) -> Result<u32, ConfigError> {
    let text = std::fs::read_to_string(path)?; // io::Error -> ConfigError
    let count: u32 = text.trim().parse()?;      // ParseIntError -> ConfigError
    if count == 0 {
        return Err(ConfigError::Missing("sensor count is zero".into()));
    }
    Ok(count)
}

When to Use unwrap, expect, and When Not To

unwrap() and expect() panic on None or Err. In industrial code, panics can crash a control system. Use them only in specific situations.

fn main() {
    // SAFE: use expect in tests or when failure is truly impossible
    let port: u16 = "8080".parse().expect("hardcoded value must parse");

    // SAFE: prototypes and quick scripts
    let config = std::fs::read_to_string("config.toml").unwrap();

    // DANGEROUS in production -- prefer proper handling
    // let reading = get_sensor_value().unwrap(); // could crash at 3 AM!
}

// Production-grade approach: always return Result
fn get_sensor_value(id: u32) -> Result<f64, String> {
    // ... real implementation ...
    Ok(42.0)
}

fn run_monitoring() -> Result<(), String> {
    let value = get_sensor_value(1)?;
    if value > 100.0 {
        println!("Alert: high reading on sensor 1");
    }
    Ok(())
}

Guidelines:

  • Use expect("reason") for values you are certain exist, with a clear message.
  • Use unwrap() only in tests or throwaway code.
  • In production industrial code, always propagate errors with ? or handle them with match.

Summary

  • Rust has no exceptions; it uses Option and Result instead, making errors visible.
  • Option represents values that may or may not exist (replaces null).
  • Result<T, E> represents operations that can succeed or fail.
  • The ? operator propagates errors cleanly without nested match blocks.
  • Custom error enums model domain-specific failures for industrial systems.
  • From trait implementations enable automatic error type conversion with ?.
  • Reserve unwrap/expect for tests and provably safe cases; production code should handle errors explicitly.
Result Option error-handling unwrap question-mark custom-errors معالجة الأخطاء النتيجة الخيار عامل الاستفهام الأخطاء المخصصة الموثوقية