Syntactic musings on the fallibility effect
— 2025-12-17

  1. defining the problem space
  2. fallible functions
  3. fallible blocks and closures
  4. operating on fallibility
  5. notes on effect naming
  6. motivating reification into an abstract type
  7. closing words

I believe it must have been about three or four years ago when Rust added the unstable yeet keyword on nightly which can be used to return new errors in try-functions. More recently Rust has added the unstable bikeshed keyword on nightly which gives try-blocks and closures the ability to express which kind of error they operate on. We’ve been deferring making decisions of error handling syntax for a while now, and I think that’s actually quite reasonable.

But if we want things to head for stable, we’re eventually going to want to decide on syntax. And so I think it’s not a bad idea to start working through the entire syntactic space here. And because things are unstable I guess it’s unavoidable to also work through the semantic space here. I guess this is a long-winded way of saying: in this post I’m sharing my opinions on Rust’s effect for fallibility. And I’m doing that mostly because I think it’s fun to think about.

Defining the problem space

For Rust we can categorize the error handling space into three categories. The first category are the places which carry the effect type notation, of which there are three. These are items in the language where for which we want to say: “Hey, this right here is operating with an error handling context”. Those items are:

The second category are the effect operators. These are used inside of the items marked with the fallibility effect, and allow us to operate on them. There are three kinds of operations we need to support:

Both of the categories we’ve seen so far apply to all of Rust’s control-flow effects (async, fallibility, and iteration). But the third category is unique to fallibility only, and that is: specialization. Fallibility has two modes it can operate in:

You can almost think of fallibility as being two separate effects in a trenchcoat. Or maybe as “concrete” being a specialized version of “abstract”. But since both flavors of fallibility are so similar in both purpose and usage, to an end-user we will both to feel similar to one another.

Fallible functions

So for the effect type we have to account for functions, closures, and blocks. For both of these we need to be able to support both abstract and concrete variants. Or put differently: we need to be able to optionally be able spell out both the explicit type when needed.

Starting with functions. Swift uses the throws keyword to annotate a function as fallible. I believe this matches Java, Kotlin, and Scala. This keyword can optionally carry a concrete type, and I think that’s perfect. Here is how I would adapt that for Rust:

fn foo() {}                       // `-> ()` (base)
fn foo() throws {}                // `-> impl Try`
fn foo() throws Err(io::Error) {} // `-> io::Result<()>`
fn foo() throws None {}           // `-> Option<()>`
fn foo() throws None -> i32 {}    // `-> Option<i32>`

The notation here borrows from pattern types. None and Err imported as part of the Rust prelude, and just like we can write None and Err are in function bodies, we should be able to use them freely in signatures as well. And since we know they are part of an impl Try, we know what the actual type is. The only open question I have is express that in the impl Try trait itself - but the Try trait is still unstable, so that’s something we can figure out.

In terms of location this follows what Swift does, listing effects after the argument list but before the arrow. This to me makes sense because effects describe properties of the function itself. And because throws says something about the function’s outputs, I expect it to be listed after the argument list. And because we don’t want it to be confused with the logical return type, it makes sense for it to be listed before the return type.

Something to be aware of also is that it’s notationally important that we are always able strip the effect from the function signature and still end up with a valid function signature. That’s an important property we need to be able to group effects together and reason about them in sets or as variables. Those include things like: effect aliases, associated effects, and effect generics:

// Adding or removing effects from functions should
// not affect the rest of the function signature:
async fn foo() -> i32;       // async effect
const fn foo() -> i32;       // const effect
fn foo() throws None -> i32; // fallible effect
fn foo() -> i32;             // no effects (baseline)

// That keeps the door open to enable higher-order reasoning
// about effects. Using a Flix-inspired notation for fun:
effect ef = async + const + throws None;
fn foo() -> i32 \ ef;

Maybe there’s something to be said for going all the way with our notation and start the migration to a general-purpose effect notation. But I’m somewhat intentionally trying to scope things down here and argue for more localized decisions on how we notate fallibility. But I can be convinced to do something more generic/consistent if people feel strongly we should do it all in one go.

Fallible blocks and closures

Blocks and closures are very similar in how they’re written; with the exception that closures must list their arguments and may list their return type. I think our block notation is a little goofy overall, and wish it would be able to list return types too. In my opinion Swift does this better in their do statements, and like I kind of wish we would have that for blocks. But we don’t do that right now, so I think we should start by spelling closures and blocks as follows:

// abstract definitions
throws {}            // block notation
throws || {}         // closure notation
throws || -> i32 {}  // explicit return type
async throws || {}   // mixing effects

// concrete definitions
throws None {}           // block notation
throws None || {}        // closure notation
throws None || -> i32 {} // explicit return type
async throws None || {}  // mixing effects

Personally I don’t love that the effects are listed here before the argument list, while in fn() {}-functions the effects are listed after the argument list (and before the arrow). And so it’s weird to me if closures don’t follow this same syntactic pattern. But I think making that more consistent is something we can probably address later.

What probably stands out here though is that I’m opting to fully omit the try keyword. The way I see it there are three options for how can we spell things here:

Personally I prefer the last one, omitting the try keyword entirely. Though I like try as a verb, it doesn’t carry any additional information that throws doesn’t already. And so I remind myself of Stroustrup’s Rule and away try goes.

Operating on fallibility

There are three kinds of operations we need to support for fallibility: create, propagate, and consume. The latter of the two we already have in the language, so let’s start there. To “forward” the fallibility effect in code, you can use what is officially called “the try propagation expression” - though it is more commonly known as “the question mark operator”:

let x = foo()?; // `?` propagates the fallibility effect

Try propagation expressions can only be used in functions which themselves return an impl Try (either abstract or concrete). If we have a function which itself is infallible, or has a different Try type, we will want to consume the fallible effect. To do that we can use the match keyword. If we are working with an abstract impl Try implementation we will first need to cast it to a concrete type. But otherwise the logic is the same for both:

// `match` consumes a concrete `Result` type
match foo() {
    Ok(x) => { .. }
    Err(err) => { .. }
}

// `match` consumes an abstract `impl Try` type
// NOTE: this should probably become a combinator on `Try`
let res = match foo().branch() {
    Break(b) => FromResidual::from_residual(b), 
    Continue(c) => Try::from_output(c),
};
match res {
    Ok(x) => { .. }
    Err(err) => { .. }
}

Then finally we have the missing “create” operator which allows you to create a new impl Try type. On unstable today this is called yeet, and though I think the name is funny, I think it should probably be called throw. Here is how you would use it inside of functions:

throw None;          // -> `return None`
throw Err("hello")   // -> `return Err("hello".into())`

And that is all three control-flow operations accounted for: ?, match, and throw.

Notes on effect naming

Though I like the throw terminology overall, I think it could do with some tidying up. If we’re going to go with throw, I want us to really lean into it. To me using try as a noun feels really unintuitive, and so I’d much rather we use nouns and verbs based on the throw operation instead:

To me this degree of regularity is incredibly appealing. Aside from the general-purpose match operation, all of these items seem clearly related to each other. And I think that’s really valuable! In a previous post I made a similar case for the iteration effect as well. Here is what I would change if I could (though I’m not saying we should):

Iterate is the name of the operation, which itself returns an iterator which can be iterated over. The async effect could probably also do with some normalization, since we’re currently mixing the terms “async”, “future”, and “poll” in ways that take time to internalize. I think some normalization of terminology here could be nice too (I’m musing, not prescribing):

I guess though if we had a “throwable” and a “waitable”, then we might also want an “iterable”. I do like “iterable” less than “iterator”, but personally I like consistency even more. Though in the grand scheme of things: this specific name doesn’t matter too much either.

Motivating reification into an abstract type

I didn’t really get into it before in this post, but I think we’re currently underestimating just how valuable it is for functions to be able to choose whether they are able to return an abstract impl Try or a concrete Try type. In my 2019 error handling survey I highlighted the auto-enums crate which is able to automatically enable heterogenous types to be returned from function bodies.

I see this combination of features becoming the language-native way to provide 80% of the value of the anyhow::Error type. Instead of casting error types to a Box<dyn Error + Send + Sync + 'static>, this would cast the error to an anonymous enum Error {} with a variant per-type. That means:

Here is an example of how I see that playing out. Function bar here can either throw i32 or throw &'static str. The idea is that the compiler would recognize this, create an anonymous enum that can hold both types, and then return that as the error type:

fn foo(x: bool) throws {  // `throws i32 | &'static str`
    bar(x)?;
}

fn bar(x: bool) throws {
    if x {
       throw 12           // `: i32`
    } else {
       throw "oops"       // `: &'static str`
    }
}

The exact rules of how this would work would be a little subtle. We likely wouldn’t be able to just to coerce errors using .into() the way we do today. But I think in the short term, we could get away with some basic rules to hold space for this in the future. So bare fn() throws {} functions:

And I believe that’s about it. But I’m keen to hear from Scott (T-Lang) to tell me exactly what I’ve missed and why this might not be enough.

Closing words

And that’s a complete design for a fallibility effect notation done. I know not everyone will like throw as much as I do, but my hope is to steer any discussion here at least to some regularity. Say if we want to go with yeet, I believe the right terminology should then become yeets/Yeetable/”re-yeet”. Personally I tend to care more about the system and framework we apply, than about the exact keyword we end up using.

Something that’s stood out while writing this is that the fallibility effect doesn’t seem to have a moral equivalent to IntoIterator/IntoFuture right now. I guess we have the automatic Into conversion that is applied when we use ?. But that is more general-purpose, and I’m having some trouble working through the exact implications of not having a separate trait.

Anyway, that’s it. I had a lot of fun writing this, and I hope it makes for an interesting read. Happy December!