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 aResult. Magnus automatically converts theErrvariant 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.")),
}
}
}