Exception Handling
In .NET, an exception is a type that inherits from the
System.Exception
class. Exceptions are thrown if a
problem occurs in a code section. A thrown exception is passed up the stack
until the application handles it or the program terminates.
Rust does not have exceptions, but distinguishes between recoverable and
unrecoverable errors instead. A recoverable error represents a problem that
should be reported, but for which the program continues. Results of operations
that can fail with recoverable errors are of type Result<T, E>
,
where E
is the type of the error variant. The panic!
macro stops
execution when the program encounters an unrecoverable error. An unrecoverable
error is always a symptom of a bug.
Custom error types
In .NET, custom exceptions derive from the Exception
class. The documentation
on how to create user-defined exceptions mentions
the following example:
public class EmployeeListNotFoundException : Exception
{
public EmployeeListNotFoundException() { }
public EmployeeListNotFoundException(string message)
: base(message) { }
public EmployeeListNotFoundException(string message, Exception inner)
: base(message, inner) { }
}
In Rust, one can implement the basic expectations for error values by
implementing the Error
trait. The minimal user-defined error
implementation in Rust is:
#[derive(Debug)]
pub struct EmployeeListNotFound;
impl std::fmt::Display for EmployeeListNotFound {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Could not find employee list.")
}
}
impl std::error::Error for EmployeeListNotFound {}
The equivalent to the .NET Exception.InnerException
property is the
Error::source()
method in Rust. However, it is not required to provide an
implementation for Error::source()
, the blanket (default) implementation
returns a None
.
Raising exceptions
To raise an exception in C#, throw an instance of the exception:
void ThrowIfNegative(int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
}
For recoverable errors in Rust, return an Ok
or Err
variant from a method:
fn error_if_negative(value: i32) -> Result<(), &'static str> {
if value < 0 {
Err("Specified argument was out of the range of valid values. (Parameter 'value')")
} else {
Ok(())
}
}
The panic!
macro creates unrecoverable errors:
fn panic_if_negative(value: i32) {
if value < 0 {
panic!("Specified argument was out of the range of valid values. (Parameter 'value')")
}
}
Error propagation
In .NET, exceptions are passed up the stack until they are handled or the program terminates. In Rust, unrecoverable errors behave similarly, but handling them is uncommon.
Recoverable errors, however, need to be propagated and handled explicitly. Their presence is always indicated by the Rust function or method signature. Catching an exception allows you to take action based on the presence or absence of an error in C#:
void Write()
{
try
{
File.WriteAllText("file.txt", "content");
}
catch (IOException)
{
Console.WriteLine("Writing to file failed.");
}
}
In Rust, this is roughly equivalent to:
fn write() {
match std::fs::File::create("temp.txt")
.and_then(|mut file| std::io::Write::write_all(&mut file, b"content"))
{
Ok(_) => {}
Err(_) => println!("Writing to file failed."),
};
}
Frequently, recoverable errors need only be propagated instead of being handled.
For this, the method signature needs to be compatible with the types of the
propagated error. The ?
operator propagates errors
ergonomically:
fn write() -> Result<(), std::io::Error> {
let mut file = std::fs::File::create("file.txt")?;
std::io::Write::write_all(&mut file, b"content")?;
Ok(())
}
Note: to propagate an error with the question mark operator the error
implementations need to be compatible, as described in a shortcut for
propagating errors. The most general
"compatible" error type is the error trait object Box<dyn Error>
.
Stack traces
Throwing an unhandled exception in .NET will cause the runtime to print a stack trace that allows debugging the problem with additional context.
For unrecoverable errors in Rust, panic!
Backtraces offer a
similar behavior.
Recoverable errors in stable Rust do not yet support Backtraces, but it is currently supported in experimental Rust when using the provide method.