Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 17: Error Handling & Robustness

Chapter Status: ✅ 100% Working (4/4 core examples)

StatusCountExamples
✅ Working4ALL error handling patterns validated
🎯 Tested4100% pass rate with 7-layer testing
⏳ Untested~5Advanced patterns (Result, Option types)
❌ Broken0ALL CORE ERROR HANDLING WORKS!

Last updated: 2025-10-13 Ruchy version: ruchy v1.84.0

Core Error Handling (4/4) - 100% Pass Rate:

  • Example 1: Safe division with guard clause ✅
  • Example 2: Input validation with range checks ✅
  • Example 3: Safe factorial with multiple guards ✅
  • Example 4: Multiple error conditions ✅

Features Validated:

  • ✅ Guard clauses with early return
  • ✅ Input validation patterns
  • ✅ Range checking
  • ✅ Safe defaults and fallbacks
  • ✅ Error message printing
  • ✅ Multiple condition checking
  • ✅ Defensive programming patterns

Working Patterns:

  • if condition checks for validation
  • Early return for error cases
  • Safe default values
  • Error messages with println
  • Cascading validation checks

The Problem

Real-world software encounters unexpected inputs, system failures, and edge cases. Writing robust applications requires systematic error handling, input validation, and graceful failure recovery. You need patterns that prevent crashes and provide meaningful feedback when things go wrong.

Quick Example

fun safe_divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        println("Error: Division by zero attempted");
        return 0; // Safe default
    }
    a / b
}

fun main() {
    let result1 = safe_divide(10, 2);   // Normal case
    let result2 = safe_divide(10, 0);   // Error case
    
    println("10 / 2 = {}", result1);   // Output: 5
    println("10 / 0 = {}", result2);   // Output: 0 (safe)
}
$ ruchy calculator.ruchy
10 / 2 = 5
Error: Division by zero attempted
10 / 0 = 0

Core Concepts

Defensive Programming Principles

  1. Validate Early: Check inputs at function boundaries
  2. Fail Gracefully: Provide meaningful error messages and safe defaults
  3. Guard Clauses: Use early returns to handle error conditions first
  4. Explicit Error Handling: Make error conditions visible and testable

Error Handling Strategies

  • Guard Clauses: Early validation and return
  • Safe Defaults: Fallback values for error conditions
  • Error Reporting: Clear messages about what went wrong
  • Input Sanitization: Clean and validate user input

Practical Usage

Input Validation Patterns

fun validate_age(age: i32) -> i32 {
    if age < 0 {
        println("Error: Age cannot be negative. Using 0.");
        return 0;
    }
    
    if age > 150 {
        println("Error: Age seems unrealistic. Using 150.");
        return 150;
    }
    
    age
}

fun calculate_retirement_year(current_age: i32) -> i32 {
    let safe_age = validate_age(current_age);
    let current_year = 2024; // Simplified
    let retirement_age = 65;
    
    if safe_age >= retirement_age {
        println("Already at retirement age");
        return current_year;
    }
    
    current_year + (retirement_age - safe_age)
}

fun main() {
    let year1 = calculate_retirement_year(30);
    let year2 = calculate_retirement_year(-5);
    let year3 = calculate_retirement_year(200);
    
    println("Retirement years: {}, {}, {}", year1, year2, year3);
}

Robust Mathematical Operations

fun safe_sqrt(x: f64) -> f64 {
    if x < 0.0 {
        println("Error: Cannot compute square root of negative number");
        return 0.0;
    }
    
    // Simple approximation for square root
    let mut guess = x / 2.0;
    let mut i = 0;
    
    while i < 10 {
        if guess * guess > x - 0.01 && guess * guess < x + 0.01 {
            return guess;
        }
        guess = (guess + x / guess) / 2.0;
        i = i + 1;
    }
    
    guess
}

fun safe_factorial(n: i32) -> i64 {
    if n < 0 {
        println("Error: Factorial undefined for negative numbers");
        return 0;
    }
    
    if n > 20 {
        println("Error: Factorial too large, computing factorial(20)");
        return safe_factorial(20);
    }
    
    if n <= 1 {
        return 1;
    }
    
    (n as i64) * safe_factorial(n - 1)
}

fun main() {
    let sqrt1 = safe_sqrt(16.0);
    let sqrt2 = safe_sqrt(-4.0);
    
    let fact1 = safe_factorial(5);
    let fact2 = safe_factorial(-3);
    let fact3 = safe_factorial(25);
    
    println("Square roots: {:.2}, {:.2}", sqrt1, sqrt2);
    println("Factorials: {}, {}, {}", fact1, fact2, fact3);
}

Array and Collection Safety

fun safe_array_access(arr: [i32; 5], index: i32) -> i32 {
    if index < 0 {
        println("Error: Array index cannot be negative");
        return arr[0]; // Return first element as default
    }
    
    if index >= 5 {
        println("Error: Array index {} out of bounds", index);
        return arr[4]; // Return last element as default
    }
    
    arr[index]
}

fun find_maximum_safe(numbers: [i32; 5]) -> i32 {
    let mut max = numbers[0];
    let mut i = 1;
    
    while i < 5 {
        if numbers[i] > max {
            max = numbers[i];
        }
        i = i + 1;
    }
    
    max
}

fun main() {
    let data = [10, 25, 5, 30, 15];
    
    let val1 = safe_array_access(data, 2);
    let val2 = safe_array_access(data, -1);
    let val3 = safe_array_access(data, 10);
    
    let maximum = find_maximum_safe(data);
    
    println("Values: {}, {}, {}", val1, val2, val3);
    println("Maximum: {}", maximum);
}

Error Recovery Patterns

Retry Logic with Limits

fun unreliable_operation(attempt: i32) -> bool {
    // Simulate an operation that fails sometimes
    if attempt < 3 {
        println("Operation failed on attempt {}", attempt);
        return false;
    }
    println("Operation succeeded on attempt {}", attempt);
    return true;
}

fun retry_with_limit(max_attempts: i32) -> bool {
    let mut attempt = 1;
    
    while attempt <= max_attempts {
        println("Attempting operation (try {})", attempt);
        
        if unreliable_operation(attempt) {
            return true;
        }
        
        attempt = attempt + 1;
    }
    
    println("Error: Operation failed after {} attempts", max_attempts);
    return false;
}

fun main() {
    let success = retry_with_limit(5);
    
    if success {
        println("✅ Operation completed successfully");
    } else {
        println("❌ Operation failed after all retries");
    }
}

Fallback and Default Values

fun get_config_value(config_name: &str) -> i32 {
    // Simulate configuration lookup
    if config_name == "timeout" {
        return 30;
    } else if config_name == "retries" {
        return 3;
    } else {
        println("Warning: Unknown config '{}', using default", config_name);
        return 0; // Safe default
    }
}

fun initialize_system() -> bool {
    let timeout = get_config_value("timeout");
    let retries = get_config_value("retries");
    let unknown = get_config_value("unknown_setting");
    
    println("System configuration:");
    println("  Timeout: {} seconds", timeout);
    println("  Retries: {} attempts", retries);
    println("  Unknown: {} (default)", unknown);
    
    // Validate configuration
    if timeout <= 0 {
        println("Error: Invalid timeout configuration");
        return false;
    }
    
    if retries < 0 {
        println("Error: Invalid retry configuration");
        return false;
    }
    
    println("✅ System initialized successfully");
    return true;
}

fun main() {
    let initialized = initialize_system();
    
    if initialized {
        println("System ready for operation");
    } else {
        println("System initialization failed");
    }
}

Input Sanitization and Validation

String Input Validation

fun sanitize_username(username: &str) -> String {
    // Check for null or empty
    if username.len() == 0 {
        println("Error: Username cannot be empty");
        return String::from("anonymous");
    }
    
    // Check length limits
    if username.len() < 3 {
        println("Error: Username too short, minimum 3 characters");
        return String::from("user123");
    }
    
    if username.len() > 20 {
        println("Warning: Username truncated to 20 characters");
        return username.chars().take(20).collect();
    }
    
    // Return sanitized username
    username.to_string()
}

fun validate_email(email: &str) -> bool {
    // Basic email validation
    if email.len() == 0 {
        println("Error: Email cannot be empty");
        return false;
    }
    
    if !email.contains('@') {
        println("Error: Invalid email format - missing @");
        return false;
    }
    
    if !email.contains('.') {
        println("Error: Invalid email format - missing domain");
        return false;
    }
    
    return true;
}

fun create_user_account(username: &str, email: &str) -> bool {
    println("Creating user account...");
    
    let safe_username = sanitize_username(username);
    let valid_email = validate_email(email);
    
    if !valid_email {
        println("❌ Account creation failed: Invalid email");
        return false;
    }
    
    println("✅ Account created for user: {}", safe_username);
    return true;
}

fun main() {
    let success1 = create_user_account("john_doe", "john@example.com");
    let success2 = create_user_account("", "invalid-email");
    let success3 = create_user_account("ab", "test@domain.co.uk");
    
    println("Account creation results: {}, {}, {}", success1, success2, success3);
}

Testing Error Conditions

Error Condition Test Patterns

fun test_division_error_handling() {
    println("Testing division error handling...");
    
    // Test normal case
    let result1 = safe_divide(10, 2);
    if result1 == 5 {
        println("✅ Normal division test passed");
    } else {
        println("❌ Normal division test failed");
    }
    
    // Test division by zero
    let result2 = safe_divide(10, 0);
    if result2 == 0 {
        println("✅ Division by zero handling passed");
    } else {
        println("❌ Division by zero handling failed");
    }
    
    // Test negative numbers
    let result3 = safe_divide(-10, 2);
    if result3 == -5 {
        println("✅ Negative number handling passed");
    } else {
        println("❌ Negative number handling failed");
    }
}

fun test_input_validation() {
    println("Testing input validation...");
    
    // Test valid age
    let age1 = validate_age(25);
    if age1 == 25 {
        println("✅ Valid age test passed");
    } else {
        println("❌ Valid age test failed");
    }
    
    // Test negative age
    let age2 = validate_age(-5);
    if age2 == 0 {
        println("✅ Negative age handling passed");
    } else {
        println("❌ Negative age handling failed");
    }
    
    // Test extreme age
    let age3 = validate_age(200);
    if age3 == 150 {
        println("✅ Extreme age handling passed");
    } else {
        println("❌ Extreme age handling failed");
    }
}

fun main() {
    test_division_error_handling();
    println("");
    test_input_validation();
    println("");
    println("🎉 Error handling tests complete!");
}

Production Error Handling Patterns

Logging and Error Reporting

fun log_error(component: &str, message: &str) {
    println("[ERROR] {}: {}", component, message);
}

fun log_warning(component: &str, message: &str) {
    println("[WARN] {}: {}", component, message);
}

fun log_info(component: &str, message: &str) {
    println("[INFO] {}: {}", component, message);
}

fun process_user_data(user_id: i32, data: &str) -> bool {
    log_info("DataProcessor", "Starting user data processing");
    
    // Validate user ID
    if user_id <= 0 {
        log_error("DataProcessor", "Invalid user ID provided");
        return false;
    }
    
    // Validate data
    if data.len() == 0 {
        log_error("DataProcessor", "Empty data received");
        return false;
    }
    
    if data.len() > 1000 {
        log_warning("DataProcessor", "Data size exceeds recommended limit");
    }
    
    // Simulate processing
    log_info("DataProcessor", "Processing data for user");
    
    // Simulate potential failure
    if user_id == 999 {
        log_error("DataProcessor", "Processing failed for user 999");
        return false;
    }
    
    log_info("DataProcessor", "Data processing completed successfully");
    return true;
}

fun main() {
    let results = [
        process_user_data(123, "valid_data"),
        process_user_data(0, "invalid_user"),
        process_user_data(456, ""),
        process_user_data(999, "test_data")
    ];
    
    let mut successful = 0;
    let mut i = 0;
    
    while i < 4 {
        if results[i] {
            successful = successful + 1;
        }
        i = i + 1;
    }
    
    println("");
    println("Summary: {}/4 operations successful", successful);
}

Error Prevention Strategies

Design by Contract

fun calculate_monthly_payment(principal: f64, rate: f64, months: i32) -> f64 {
    // Preconditions - validate inputs
    if principal <= 0.0 {
        println("Error: Principal must be positive");
        return 0.0;
    }
    
    if rate < 0.0 {
        println("Error: Interest rate cannot be negative");
        return 0.0;
    }
    
    if months <= 0 {
        println("Error: Loan term must be positive");
        return 0.0;
    }
    
    // Handle edge case of zero interest
    if rate == 0.0 {
        return principal / (months as f64);
    }
    
    // Calculate monthly payment
    let monthly_rate = rate / 12.0;
    let payment = principal * monthly_rate *
        ((1.0 + monthly_rate) ** (months as f64)) /
        (((1.0 + monthly_rate) ** (months as f64)) - 1.0);
    
    // Postcondition - validate result
    if payment <= 0.0 {
        println("Error: Calculated payment is invalid");
        return 0.0;
    }
    
    payment
}

fun main() {
    let payment1 = calculate_monthly_payment(100000.0, 0.05, 360);
    let payment2 = calculate_monthly_payment(-1000.0, 0.05, 360);
    let payment3 = calculate_monthly_payment(50000.0, 0.0, 60);
    
    println("Monthly payments: {:.2}, {:.2}, {:.2}", payment1, payment2, payment3);
}

Summary

  • Guard clauses provide early validation and clear error paths
  • Safe defaults prevent crashes when errors occur
  • Input validation catches problems at system boundaries
  • Error logging provides visibility into system behavior
  • Retry patterns handle transient failures gracefully
  • Design by contract validates preconditions and postconditions
  • Testing error paths ensures robust behavior under failure
  • Defensive programming validates inputs systematically

Error handling in ruchy transforms experimental code into production-ready applications by systematically addressing failure scenarios and providing graceful recovery mechanisms.