<import "base.kuht" as "base" />
<head>
<title>How to Introduce try-catch to Rust</title>
<meta name="description" content="Don't do this, but the door is open for it." />
</head>
<body>
<article>
<h1>How to Introduce try-catch to Rust</h1>
<p>
As a disclaimer, this isn't my personal proposal or anything. But I took a look at the RFC for try
blocks in Rust, and I thought it was interesting how it it left the door open for catch blocks in the
future. I thought it would be an interesting idea to share. But in order to talk about this, we have
to work our way forwards.
</p>
<h2>Above the Iceberg: Rust enums</h2>
<p>
Many of you readers will already understand this much, but if you've never worked with Rust before,
I'll have to introduce the enum system. Rust's enums can carry values on particular variants. They're
pretty much just tagged or discriminated unions.
</p>
<pre>
pub enum JsonValue {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<JsonValue>),
Object(Vec<(String, JsonValue)>),
}
</pre>
<p>
There are a few ways we can get the inner value. We can use a match expression, which is a bit like
switch in other languages.
</p>
<pre>
fn to_json(value: JsonValue) -> String {
match value {
JsonValue::Null => "null".to_string(),
JsonValue::Boolean(b) => b.to_string(),
JsonValue::Number(n) => n.to_string(),
JsonValue::String(s) => format!("\"{s}\""),
JsonValue::Array(a) => {
let mut builder = String::from("[ ");
for element in a {
builder.push_str(&to_json(element));
builder.push_str(", ");
}
builder.push(']');
builder
}
JsonValue::Object(o) => {
let mut builder = String::from("{ ");
for (key, value) in o {
builder.push_str(&format!("\"{key}\": {}", to_json(value)));
builder.push_str(", ");
}
builder.push('}');
builder
}
}
}
</pre>
<p>We can use if let to match on only one particular variant.</p>
<pre>
fn unwrap_number(value: JsonValue) -> f64 {
if let JsonValue::Number(n) = value {
n
} else {
panic!("The passed value was not a number");
}
}
</pre>
<p>
More recently, we got let else, which lets us assert that it is one particular variant, or quit early.
</p>
<pre>
fn unwrap_number(value: JsonValue) -> f64 {
let JsonValue::Number(number) = value else {
panic!("The passed value was not a number");
}
number
}
</pre>
<p>But importantly, there's no way to get the inner value without first checking for the variant.</p>
<h2>In the Beginning: The <code>Result</code> type</h2>
<p>
Rust doesn't have exceptions. The <code>panic</code> macro, shown earlier, causes an immediate end to
the program (in most cases). For catchable errors, we use the <code>Result</code> type, which is
included in Rust's core library.
</p>
<pre>
pub enum Result<T, E> {
Ok(T),
Err(E),
}
</pre>
<p>
The <code>Result</code> type has two generic types: <code>T</code>, which is the type that was
expected to be returned; and <code>E</code>, which is the type that will be returned instead if an
error occurred. For example, we could define a <code>parse_number</code> function like this:
</p>
<pre>
fn parse_to_f64(s: &str) -> Result<f64, ParseFloatError>
</pre>
<p>
If the passed string is a number, then this function will return a 64-bit float. Otherwise, it will
return a <code>ParseFloatError</code>, which is an error describing a failure to parse the number.
We can use the result of this function like so:
</p>
<pre>
fn foo() {
let number = match parse_to_f64("3.14159") {
Ok(number) => number,
Err(e) => panic!("{e}"),
};
println!("{number}");
}
</pre>
<p>
The <code>Result</code> type has a convenient helper method to do this, called <code>unwrap</code>.
If the result is <code>Ok</code>, then the inner value will be returned. Otherwise, the program will
panic.
</p>
<pre>
fn foo() {
let number = parse_to_f64("3.14159").unwrap();
println!("{number}");
}
</pre>
<p>But typically, you'd want to just throw the error again.</p>
<pre>
fn foo() -> Result<(), ParseFloatError> {
let number = match parse_to_f64("3.14159") {
Ok(number) => number,
Err(e) => return Err(e),
};
println!("{number}");
}
</pre>
<h2>Introduced Later: The <code>?</code> operator</h2>
<p>
As you might imagine, using that match expression over and over again to propogate errors will
quickly become tedious. The original solution was to use the <code>try</code> macro to do it for you.
</p>
<pre>
fn foo() -> Result<(), ParseFloatError> {
let number = try!(parse_to_f64("3.14159"));
println!("{number}");
}
</pre>
<p>
But the 2018 edition of Rust introduced some syntactic sugar to make this even easier. It can be used
to propogate an error in any function which returns a similar <code>Result</code>.
</p>
<pre>
fn foo() -> Result<(), ParseFloatError> {
let number = parse_to_f64("3.14159")?;
println!("{number}");
}
</pre>
<p>It can even convert between errors, if possible.</p>
<pre>
enum JsonError {
ParseNumber(ParseFloatError),
Other(String),
}
impl From<ParseFloatError> for JsonError {
fn from(value: ParseFloatError) -> Self {
Self::ParseNumber(value)
}
}
fn foo() -> Result<(), JsonError> {
// automatically converts the ParseFloatError to a JsonError
let number = parse_to_f64("3.14159")?;
println!("{number}");
}
</pre>
<p>It also works on the <code>Option</code> type, which is also included in Rust.</p>
<pre>
fn foo() -> Option<i32> {
let array = [1, 2, 3, 4];
let x = array.get(4)?; // get the element at index 4, which doesn't exist in the array
Some(x)
}
</pre>
<p>
And some of you may be surprised to learn that it also works on <code>ControlFlow</code> and some
<code>Poll</code> types.
</p>
<p>
This isn't stable yet, but there's also a proposal to allow the use of the <code>?</code> operator on
custom types. This would be done using a few new traits: <code>Residual</code>,
<code>FromResidual</code>, and <code>Try</code>. A residual is a type that can be returned from using
a <code>?</code>. The <code>FromResidual</code> trait is implemented on types that can be created
from residuals (for example, using <code>?</code> to get a
<code>Result<Infallible, JsonError></code>, then converting that to a
<code>Result<(), JsonError></code>). The <code>Try</code> trait is implemented when a
<code>FromResidual</code> type can also have the <code>?</code> operator applied to it. These traits
are already available in nightly Rust. As an exercise for the Dallas Rust User Group, we created
<a href="https://github.com/joshuaPurushothaman/diy-result">our own <code>Result</code> type</a>, and
used these traits to make the <code>?</code> operator work with it.
</p>
<h2>In Nightly: Try Blocks</h2>
<p>
Today, <code>try</code> is a reserved keyword. So to use the <code>try!</code> macro in newer
editions of Rust, you'll need to use the raw-literal syntax.
</p>
<pre>
fn foo() -> Result<(), ParseFloatError> {
let number = r#try!(parse_to_f64("3.14159"));
println!("{number}");
}
</pre>
<p>
What's the keyword being used for? It's for the try-block feature, which is currently only available
in nightly. The idea for this was made at roughly the same time as the <code>?</code> operator, to
catch anything that was propogated with it. However, only the <code>?</code> got merged.
</p>
<pre>
let x: Result<_, _> = try { foo()?.bar()?.baz()? };
</pre>
<p>This way, you can catch an error without having to create a whole new function.</p>
<h2>The Future: Catch blocks</h2>
<p>
From what I understand, the original idea was to not reserve <code>try</code> as a keyword. Instead,
the keyword was going to be <code>catch</code>. That idea was dropped, presumably because it would
break with the convention created by every other programming language. But this also leaves the door
open to introduce catch blocks later.
</p>
<p>
I haven't found any proposed RFCs for what catch blocks might look like. I think most people don't
want to think about this until <code>try</code> is finished. But people have noted that we have the
option of adding catch later, and the rough syntax isn't too hard to figure out.
</p>
<pre>
let x: Result<f64, _> = try {
//...
} catch error {
panic!("Unexpected error: {:?}", error);
}
</pre>
<p>We could pattern match on the expression too.</p>
<pre>
let x: f64 = try {
//...
} catch JsonError::ParseNumber(error) {
panic!("Failed to parse float: {:?}", error);
} catch JsonError::Other(error) {
panic!("An unexpected error occurred: {:?}", error);
}
</pre>
<p>We'd also have to allow try-catch on <code>Option</code>.</p>
<pre>
let array = [1, 2, 3, 4];
let x = try {
array.get(4)?;
} catch { // note that there's no reason for a variable to be created here
// this would probably desugar to `catch _`
// that desugaring would allow this syntax to also be used with Result
panic!("wait, what am i doing with my life?");
}
</pre>
<p>
Creating this API would probably also mean we'd have to think about adding a <code>Catch</code>
trait, so that it could be allowed on custom types. Modeling this is difficult, but can be done.
</p>
<pre>
pub trait Catch: Try {
type Error;
fn error(residual: Self::Residual) -> Self::Error;
}
impl<T, E> for Result<T, E> {
type Error = E;
fn error(residual: Self::Residual) -> Self::Error {
let Err(error) = self;
error
}
}
impl<T> for Option<T> {
type Error = ();
fn error(residual: Self::Residual) -> Self::Error {
()
}
}
</pre>
<p>In case you're wondering about the desugaring:</p>
<pre>
let x: f64 = match try { /* ... */ }.branch() {
ControlFlow::Continue(output) => output,
ControlFlow::Break(residual) => match Result::error(residual) {
JsonError::ParseNumber(error) => panic!("Failed to parse float: {:?}", error),
JsonError::Other(error) => panic!("An unexpected error occurred: {:?}", error),
}
}
</pre>
<p>
Does that mean that <code>finally</code> could be added in the future too? Perhaps. I've never been a
big fan of finally blocks. But we can do it. If you're familiar with the
<a href="https://lib.rs/scopeguard">scopeguard</a> crate, then it shouldn't seem too difficult.
</p>
<pre>
let x: f64 = try {
//...
} catch JsonError::ParseNumber(error) {
panic!("Failed to parse float: {:?}", error);
} catch JsonError::Other(error) {
panic!("An unexpected error occurred: {:?}", error);
} finally {
println!("finally! we're done!");
}
// desugars to...
let x: f64 = {
defer! {
println!("finally! we're done!");
}
match try { /* ... */ }.branch() {
ControlFlow::Continue(output) => output,
ControlFlow::Break(residual) => match Result::error(residual) {
JsonError::ParseNumber(error) => panic!("Failed to parse float: {:?}", error),
JsonError::Other(error) => panic!("An unexpected error occurred: {:?}", error),
}
}
}
</pre>
<h2>Yeet! We can go even further</h2>
<p>
Now that we have most of the syntax from languages that have exceptions, let's go even further.
We could consider adding the <code>throw e</code> keyword as syntactic sugar for
<code>return Err(e)</code>.
</p>
<pre>
let x: f64 = try {
throw JsonError::Other("oh no, an error!".to_string());
} catch JsonError::ParseNumber(error) {
panic!("Failed to parse float: {:?}", error);
} catch JsonError::Other(error) {
panic!("An unexpected error occurred: {:?}", error);
} finally {
println!("finally! we're done!");
}
</pre>
<p>
This isn't available in nightly, because <code>throw</code> isn't yet a keyword in Rust. Worse, we
can't add it as a keyword until everybody agrees on what keyword to use. Some languages, such
Python, use <code>raise</code> instead.
</p>
<p>
This problem is known as bikeshedding. People can't agree on what color to paint the bikeshed, so the
bikeshed never gets built. Everybody agrees that having the bikeshed is important, regardless of what
color it is. A common solution to this is to pick an option nobody would ever want. This lets everyone
know that we're not actually going to use that color, so we can and will change it later. This
happened.
</p>
<pre>
#![feature(yeet_expr)]
fn throw_error() -> Result<Infallible, JsonError> {
do yeet JsonError::Other("an error has been yeeted");
}
</pre>
<p>
Yep, <code>yeet</code> is the chosen keyword. The <code>do</code> needs to be there because
<code>yeet</code> is not a reserved word, but <code>do</code> is, despite it not being used for
anything.
</p>
<p>
The way this works under the hood, is <code>do yeet e</code> desugars to <code>Yeet(e)</code>.
Then a couple of <code>FromResidual</code> implementations allow the <code>Yeet</code> type to be
used for error propogation.
</p>
<pre>
impl<T> FromResidual<Yeet<()>> for Option<T> {
fn from_residual(_: Yeet<()>) -> Option<T> {
None
}
}
impl<T, E, F: From<E>> FromResidual<Yeet<E>> for Result<T, F> {
fn from_residual(error: Yeet<E>) -> Result<T, F> {
Err(error.0.into())
}
}
</pre>
<p>
Now that we've been using <code>yeet</code> for so long, there are some people who want this to be
the new keyword. There was even a proposal to reserve yeet for Rust 2021. It was rejected.
</p>
<h2>Now in Reverse? What?</h2>
<p>
I had to share this because Rust does not have exceptions. The idea that we could introduce an
exception-like syntax for a language which doesn't have them is amazing to me.
</p>
<p>This got me thinking, could we introduce <code>Result</code> to a language that doesn't have it?</p>
<p>Imagine the following C# code, where using try, but without a catch, returns a <code>Result</code>.</p>
<pre>
// I used a wildcard for the error type because C# can throw one of multiple exceptions here
Result<decimal, _> = try {
yield demical.Parse("3.14159");
}
</pre>
<p>
This would be easier in languages where block expressions exist. If a language were to pursue this,
I'd prefer they call the type <code>Except</code> or <code>Exceptional</code> instead of
<code>Result</code>.
</p>
<h2>Conclusion</h2>
<p>
The main I reason I wrote this post was because, when I proposed a new scripting language in an
<a href="/blog/best-tool-for-the-job.html">earlier blog post</a>, I wanted to talk about how
try/catch would be implemented in it. But I figured that was way too off-topic for that post. So I
wanted to discuss it here.
</p>
<p>
My scripting language would have unchecked exceptions, with the option to catch errors and put them
inside of an <code>Exceptional</code> type.
</p>
<pre>
function throws() {
throw "oh no"
}
throws() // this crashes the script
</pre>
<p>Wrapping an expression in a try block yields an <code>Except</code> type.</p>
<pre>
function throws() {
fail with "oh no"
}
let x: Except<any, any> = try {
throws() // this doesn't crash
}
</pre>
<p>I think it's feasible to use a postfix <code>!</code> as syntactic sugar for this</p>
<pre>
function throws() {
fail with "oh no"
}
let x: Except<any, any> = throws()!
</pre>
<p>Then with <code>catch</code> or <code>.catch</code>, the error can be handled</p>
<pre>
function throws() {
fail with "oh no"
}
let x: Except<any, any> = throws()!.catch error {
print("an error occurred")
exit()
}
</pre>
<p>I like this idea. I'm glad I could share it with you.</p>
</article>
</body>
|