Skip to main content

Error Handling

This guide covers how to handle errors in Rust and correctly map them to Ruby exceptions. Proper error handling is critical to prevent crashes in the Ruby VM.

Result vs. panic!

  • Result<T, magnus::Error>: For recoverable errors. Functions that can fail should return a Result. Magnus automatically converts the Err variant into a Ruby exception that can be rescued in Ruby code.
  • panic!: For unrecoverable errors (i.e., bugs). A panic in your Rust code will crash the entire Ruby process. Avoid panics in production code.

Mapping Rust Errors to Ruby Exceptions

Return a magnus::Error to raise a Ruby exception. You can create an error from any of Ruby's standard exception classes.

use magnus::{Error, Ruby};

fn divide(ruby: &Ruby, a: f64, b: f64) -> Result<f64, Error> {
if b == 0.0 {
Err(Error::new(ruby.exception_zero_div_error(), "Division by zero")) // Becomes `ZeroDivisionError` in Ruby.
} else {
Ok(a / b)
}
}

fn get_first(ruby: &Ruby, array: &[i64]) -> Result<i64, Error> {
array.get(0).copied().ok_or_else(|| {
Error::new(ruby.exception_index_error(), "index 0 out of bounds") // Becomes an `IndexError`.
})
}

Custom Exception Classes

Define your own exception classes in a module to provide more specific error information.

use magnus::{prelude::*, Error, Ruby, ExceptionClass, RModule}; // Add ExceptionClass, RModule

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = ruby.define_module("MyGem")?;
let _custom_error = module.define_error("CustomError", ruby.exception_standard_error())?; // Fix deprecated exception
Ok(()) // You can now raise `MyGem::CustomError`.
}

fn do_something(ruby: &Ruby) -> Result<(), Error> {
// Correct way to get nested class in magnus 0.8
let my_error_module: RModule = ruby.class_object().const_get("MyGem")?;
let my_error_class: ExceptionClass = my_error_module.const_get("CustomError")?;
Err(Error::new(my_error_class, "something went wrong"))
}

Propagating Errors with ?

The ? operator is the idiomatic way to propagate errors up the call stack.

use magnus::{Error, Ruby, Value};

fn step_one(_ruby: &Ruby, input: Value) -> Result<Value, Error> { Ok(input) } // ... might fail
fn step_two(_ruby: &Ruby, input: Value) -> Result<Value, Error> { Ok(input) } // ... might fail

fn multi_step_process(ruby: &Ruby, value: Value) -> Result<Value, Error> {
let step1_result = step_one(ruby, value)?; // Returns early on error.
let step2_result = step_two(ruby, step1_result)?;
Ok(step2_result)
}

Handling Panics

If you must use a library that can panic, wrap the call in std::panic::catch_unwind to convert the panic into a Ruby RuntimeError.

use magnus::{Error, Ruby};
use std::panic::catch_unwind;

fn might_panic() { panic!("this should not happen"); }

fn safe_wrapper(ruby: &Ruby) -> Result<(), Error> {
let result = catch_unwind(|| { might_panic(); });
match result {
Ok(_) => Ok(()),
Err(_) => Err(Error::new(ruby.exception_runtime_error(), "Internal Rust panic occurred.")),
}
}

Handling RefCell Borrows

Incorrect RefCell usage can cause panics. Use try_borrow_mut to handle borrow errors gracefully.

use std::cell::RefCell;
use magnus::{Error, Ruby};

#[magnus::wrap(class = "Counter")]
struct Counter(RefCell<i64>);

impl Counter {
fn try_increment(&self, ruby: &Ruby) -> Result<(), Error> {
match self.0.try_borrow_mut() {
Ok(mut value) => { *value += 1; Ok(()) },
Err(_) => Err(Error::new(ruby.exception_runtime_error(), "Counter is already borrowed.")),
}
}
}