Exception Handling
In JavaScript, an exception should always be an Error
object or an instance of an Error
subclass. 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
An example on how to create user-defined exceptions:
class EmployeeListNotFoundException extends Error {
constructor(message) {
super(message);
this.name = 'EmployeeListNotFoundException';
}
}
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 JavaScript Error.cause
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 error in JavaScript, throw an error:
function throwIfNegative(value) {
if (value < 0) {
throw new Error('Value cannot be negative');
}
}
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 JavaScript, exceptions are passed up 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 JavaScript:
//JavaScript doesn't have a file system in it. People often implement file systems using the BrowserFS library that mimic Node.js APIs.
function write() {
try {
fs.writeFileSync('file.txt', 'content');
} catch (error) {
console.log('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
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.