← Back to Examples

Exercise: Calculator with Error Handling

calculator
⭐⭐ intermediate ⏱️ 25 min 📚 Requires: hello-world

Now that you can write basic functions, let's tackle something more challenging: a calculator that handles errors gracefully.

In real-world code, things go wrong. Users pass invalid input. Operations fail. Good Rust code uses the type system to handle these cases explicitly.

🎯 Learning Objectives

Thinking

Doing

💬 Discussion

src/lib.rs

💡 Hints

Hint 1: Using match on enums

Use a match expression to handle each operation:

match op {
    Operation::Add => { /* ... */ },
    Operation::Subtract => { /* ... */ },
    Operation::Multiply => { /* ... */ },
    Operation::Divide => { /* ... */ },
}

Each arm should return a Result<f64, CalculatorError>.

Hint 2: Handling Division

For division, check for zero before performing the operation:

Operation::Divide => {
    if b == 0.0 {
        return Err(CalculatorError::DivisionByZero);
    }
    Ok(a / b)
}

Note: We check b == 0.0 before dividing to prevent the error.

Hint 3: Complete Implementation
pub fn calculate(a: f64, b: f64, op: Operation) -> Result<f64, CalculatorError> {
    match op {
        Operation::Add => Ok(a + b),
        Operation::Subtract => Ok(a - b),
        Operation::Multiply => Ok(a * b),
        Operation::Divide => {
            if b == 0.0 {
                Err(CalculatorError::DivisionByZero)
            } else {
                Ok(a / b)
            }
        }
    }
}
⚠️ Try the exercise first! Show Solution
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Operation {
    Add,
    Subtract,
    Multiply,
    Divide,
}

#[derive(Debug, Clone, PartialEq)]
pub enum CalculatorError {
    DivisionByZero,
    Overflow,
}

impl fmt::Display for CalculatorError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CalculatorError::DivisionByZero => write!(f, "Cannot divide by zero"),
            CalculatorError::Overflow => write!(f, "Arithmetic overflow occurred"),
        }
    }
}

impl std::error::Error for CalculatorError {}

pub fn calculate(a: f64, b: f64, op: Operation) -> Result<f64, CalculatorError> {
    match op {
        Operation::Add => Ok(a + b),
        Operation::Subtract => Ok(a - b),
        Operation::Multiply => Ok(a * b),
        Operation::Divide => {
            if b == 0.0 {
                Err(CalculatorError::DivisionByZero)
            } else {
                Ok(a / b)
            }
        }
    }
}

Explanation

Why use Result instead of panicking?

In Rust, we distinguish between:

  • Recoverable errors: Use Result<T, E> - the caller can handle the error
  • Unrecoverable errors: Use panic! - the program cannot continue

Division by zero is recoverable - the caller might want to show an error message, use a default value, or try a different calculation.

Why check before dividing?

With floating-point numbers, 10.0 / 0.0 actually produces inf (infinity) rather than panicking. But for a calculator, we want to treat this as an error. By checking first, we can return a clear error message.

The match pattern

Each arm of the match returns the same type: Result<f64, CalculatorError>. This is enforced by the compiler - all arms must return the same type.

Error type design

We created a custom CalculatorError enum rather than using a string. This:

  • Allows pattern matching on specific error types
  • Provides type safety
  • Implements std::error::Error for compatibility with ? operator

🧪 Tests

Run these tests locally with:

cargo test
View Test Code
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_addition() {
        assert_eq!(calculate(5.0, 3.0, Operation::Add), Ok(8.0));
    }

    #[test]
    fn test_subtraction() {
        assert_eq!(calculate(10.0, 4.0, Operation::Subtract), Ok(6.0));
    }

    #[test]
    fn test_multiplication() {
        assert_eq!(calculate(7.0, 6.0, Operation::Multiply), Ok(42.0));
    }

    #[test]
    fn test_division() {
        assert_eq!(calculate(15.0, 3.0, Operation::Divide), Ok(5.0));
    }

    #[test]
    fn test_division_by_zero() {
        assert_eq!(
            calculate(10.0, 0.0, Operation::Divide),
            Err(CalculatorError::DivisionByZero)
        );
    }

    #[test]
    fn test_negative_numbers() {
        assert_eq!(calculate(-5.0, 3.0, Operation::Add), Ok(-2.0));
    }

    #[test]
    fn test_floating_point() {
        let result = calculate(0.1, 0.2, Operation::Add).unwrap();
        assert!((result - 0.3).abs() < 0.0001);
    }
}

🤔 Reflection