diff options
| author | Mica White <botahamec@outlook.com> | 2026-09-05 21:29:35 -0400 |
|---|---|---|
| committer | Mica White <botahamec@outlook.com> | 2026-09-05 21:29:35 -0400 |
| commit | 5ec2d93446ae890e164dd8ad33602a09c0d8814e (patch) | |
| tree | 6e863c1f7add4f6d682b280f4d9d18669fcc750b /src/blog/try-catch-in-rust.kuht | |
First commit
Diffstat (limited to 'src/blog/try-catch-in-rust.kuht')
| -rwxr-xr-x | src/blog/try-catch-in-rust.kuht | 553 |
1 files changed, 553 insertions, 0 deletions
diff --git a/src/blog/try-catch-in-rust.kuht b/src/blog/try-catch-in-rust.kuht new file mode 100755 index 0000000..63734cd --- /dev/null +++ b/src/blog/try-catch-in-rust.kuht @@ -0,0 +1,553 @@ +<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> |
