A brief apology of Ok-Wrapping

I’ve long been a proponent of having some sort of syntax in Rust for writing functions which return results which “ok-wrap” the happy path. This is has also always been a feature with very vocal, immediate, and even emotional opposition from many of our most enthusiastic users. I want to write, in one place, why I think this feature would be awesome and make Rust much better.

I don’t want to get into the details too much of the specific proposal, but here’s a sketch of one way this could work (there are a number of variables). We would add a syntactic modifier to the signature of a function, like this:

fn foo() -> usize throws io::Error {
    //..
}

This function returns Result<usize, io::Error>, but internally the return expressions return a value of type usize, not the Result type. They are “Ok-wrapped” into being Ok(usize) automatically by the language. If users wish to throw an error, a new throw expression is added which takes the error side (the type after throws in the signature). The ? operator would behave in this context the same way it behaves in a function that returns Result.

If this is your first introduction to this topic, you should know it has a very long history of discussion that preceeds this blog post. We’ve been having discussions on this theme since around the 1.0 release of the language, and they’ve never made any progress.

I find these discussions exhausting because I feel my position is consistently misrepresented, often by people using very emotionally charged language, sometimes suggesting (or in some cases outright stating) that I am ruining Rust, or at least proposing to, or maybe just leading everyone astray. This is quite frustrating since I have devoted a lot of time - for some periods paid, but for many (including at this moment) unpaid - over the past 5 years to try to improve Rust, and I think I’ve made a lot of very valuable contributions.

Some very kind and generous people try to counteract this by always showing gratitude toward me when I get these sorts of responses. I appreciate their effort and it makes me think better of them, but I have to be honest that it doesn’t realy make me feel better. Gratitude is very weak in the face of ungratitude. So I’d say, not only on this issue but on all issues, would be for everyone to take the responsibility for how their comments will be recieved, to demonstrate emotional maturity and respect for other people, and to be empathetic with the person whose work they are commenting on. Also, put a bit of effort to be more confident that you understand the work you’re commenting on before you make your comment.

Ok wrapping would be a huge boon

I have a file in a project I’ve been working on (not open source yet) which feels pretty representative of the kind of code I write. It contains a lot of functions, some of which return Result and some of which don’t. As I edit it this keeps changing, as I introduce new code that could raise errors, and realize new points at which errors should be “caught” and no longer returned.

Because I’m using fehler, each time a function “enters or exits the Result monad” (as it were), I only make one edit: I add or remove the #[throws] annotation on that function. This is the power of Ok wrapping that I never see acknowledged: without Ok wrapping, users need to 1 + N edits, for every happy path return in their function, every time they change whether this function returns a Result.

The file in question contains 16 functions. Here is the distribition of happy path returns among those functions:

       Number of functions with..  │  ..this many happy path returns
      ─────────────────────────────┼───────────────────────────
                               8   │   1
                               4   │   2
                               1   │   4, 5, 7, and 17

Half of my functions only have 1 return (so that’s only twice as many edits without ok-wrapping), but a few of them (which you can imagine, are quite central to the operation of this module) have many paths. These functions are the real source of pain without ok-wrapping.

Most of my functions with many return paths terminate with a match statement. Technically, these could be reduced to a single return path by just wrapping the whole match in an Ok, but I don’t know anyone who considers that good form, and I certainly don’t. But an experience I find quite common is that I introduce a new arm to that match as I introduce some new state to handle, and handling that new state is occassionally fallible.

Without fehler, this becomes an ordeal as I now edit the 17 other match arms to be wrapped in Ok, in addition to changing the function signature. With fehler, I just change the function signature to document that this function is now fallible.

And of course, an experience I have as well is being uncertain if this error should be thrown, if it should be caught, if we need to call the fallible function in this branch at all, if maybe that function should be catching the error - etc. And without fehler I’d be making 18 edits every time I change my mind.

And not for nothing, these are not very easy edits to make because they require a wrap-around call that involves making edits in multiple places: exactly the reason ? was preferred to try!() in the first place. Of course I already know that someone will say we just need a postfix operator for the Ok constructor, and while that would strictly speaking be an improvement, it wouldn’t collapse the number of edits from O(n) in the number of return paths to O(1).

I wouldn’t be surprised if, right now, people are putting error boundaries in suboptimal places because the edit cost of changing those boundaries to the better position is too high! I’m sure I’ve done it. How terrible!

It would be consistent with other effects/monads

These are the core “effects/monads” of Rust as most people use it today:

  • Option
  • Result
  • Iterator
  • Future
  • Stream

We have seen with Future, and will someday see with Iterator and Stream, a syntactic pattern for dealing with “being in the monad” in Rust:

  • A syntactic annotation on the function which is “in the monad” (marking the function async, fallible, a generator, etc)
  • An annotation for “introducing an effect” (throwing an error, yielding a value)
  • An annotation for “forwarding an effect” from a subroutine (?, await, Python’s yield from)

Let me just say, I’m not very optimistic about any proposals to introduce polymorphism over these different effects. There are two different avenues these proposals take.

First is some sort of “Monad trait.” There have been proposals for how this could be done with the addition of a heapful of new abstractions in Rust’s trait system, but I’m very suspicious of their ability to integrate well with type inference, unification, etc. I suspect any modelling like that would be incredibly unergonomic to actually use, because you’d have to type ascribe loads of things.

Second would be proposals to add a new axis of polymorphism over effects. This seems like it has the possiblity of at least not being terribly unergonomic, but I see two problems. The first is that it needs to be done in a way which reduces, rather than increases, cognitive load, which is going to depend a lot on the syntax we choose and so on. The second is that there’s just no bandwidth to implement this sort of thing in any forseeable time horizon. Ask me again in 3 years, maybe.

Despite this, the fundamental consistency is valuable for reducing cognitive load. One response I often see to talk of this feature is that it make Rust harder to teach, but I think exactly the opposite is true. Right now, ? is difficult to teach because you have to teach it in terms of matching on a Result. This would be as if you add to teach await in terms of polling a future, and it’s because we have an incomplete effect system for fallibility - we only have the notation for forwarding the effect, we don’t have the notation for being effectful.

Instead, since most code would just use some combination of ?, throw and throws, new users could have an intuitive understanding of fallibility in the same way async/await gives them an intuitive understanding of asynchrony. They would learn about how to process failure and “leave the monad” at the second stage of their understanding, in the same way that they learn about the task system at the second stage of understanding async/await (but here the cliff would also be much lower, because Result is much simpler than Future).

It could go hand in hand with ABI-based optimizations

Panicking is actually faster in many cases than using Result, because the happy path is cheaper. If there are many function calls between where an error is raised and where it is handled, it can result in faster code to not have to check the result value for fallibility at all of those calls during the happy path.

This is along the same lines as why I recommend using a trait object for errors in application code: making the happy path as cheap as possible is a huge win in cases where errors represent an uncommon systemic failure (whether programmer error or an issue arising from another program in a distributed system). By using anyhow::Error, for example, the error type’s stack representation is just a single pointer, which avoids making the Result’s stack representation unnecessarily large.

But by just eliminating the Result type from the representation at all at an ABI level, we could make things even better. It is not impossible to imagine an optimization pass which converts a use of the Result pattern into a stack unwinding pattern on platforms which support onwinding, when it has no effect on program behavior, Syntactically nothing is different, but users could get the performance benefits of exceptions when they would benefit.

This is orthogonal to the syntactic issue - these changes can be made completely independent of one another - but it would involve exactly the same type of language integration as the syntactic change, and opposition to this kind of language integration is usually one of the main objections.

Counterarguments to common objections

The error path should not be invisible

This is the most common counterargument and it is very easy to respond to: I agree that the error path should not be invisible. That’s why I’ve never advocated making any change to how the error path is handled. It is still necessary to use ? to “rethrow” an exception.

And yet it’s always one of the first and most popular comments any time this discussion is brought up again. I wish people would put a bit more effort in understanding things before making comments on them, but all evidence suggests this is not the natural behavior of humans on the internet.

Let me just repeat: no one is proposing to make the error path invisible. If you think that is the consequence of throwing/catching function proposals, you have misunderstood these proposals and you should start again.

I actually find this complaint quite ironic, because ok-wrapping syntax would make it easier to identify all of the error paths because of the way it interacts with implicit final return. Today, when a function call returning a result ends in an implicit return, if that function is also returning a result, it is totally unmarked. But this syntax would require, even in that case, that that terminal function call be annotated with ?, identifying it as fallible. With this syntax, now every fallible path is marked with ? or a throw expression. This is more consistent and more explicit!

Having to edit the happy path returns adds meaningful value

I’ll have to be honest, a large part of the time this argument is made, I just don’t understand the value the person is claiming that this adds. Usually they are using terminology that feels handwavy and ideological to me, and I cannot find a basis in concrete user experience from which to judge the value they claim is added.

There is one argument along these lines that I have understood practically, which is this: suppose I am adding a fallible case to a function. Perhaps this is a scenario in which now, every other return path should be examined to see if it should in fact stop being an ok-path return, but instead now return an error. In such a case, applying Ok to each of those return paths gives me an opportunity to make that examination.

First of all, I don’t think I have ever experienced this scenario in my life. In any case it is diminishingly uncommon, whereas the example I described above of needing to move things in and out of the Result monad is something I experience with extreme frequency. This alone is enough for my to deprioiritize this problem, but I have considered the possibility, and I would make these further comments.

If you have a return path you want to move from the happy path to the error path, values of that path must have been represented in the return type before. If you were using Rust’s type system at all effectively, this would probably mean modifying the return type to remove those values from its set of values (for example, perhaps previously one variant of your enum you now realize should actually be a separate error type). By making this edit to the type definition, you can follow the compiler’s guidance to find the happy paths that should now be error paths, which is far better than a manual and error-prone examination of every happy path anyway.

If you were previously representing errors in a way that had no type assistance whatsoever (for example, you were just returning an i32 that is negative in error cases), well, yea, you get no help. But this code would be extremely unidiomatic, and it would be obvious to you that you need to transform this function carefully. In that case, you should not just make a change to mark your function as “throwing,” but use an “explicit Result” intermediate step to help you catch all of the return paths and examine them. But this feels like an extremely uncommon problem with obvious alerts to danger.

I don’t like exception terminology

I’m completely open to different terminology if someone can come up with something good. Most counterproposals have also modified return, in a fallible context, because they are proposed by people who hold to the previous objection. Since I think modifying return would annull the key benefit of this proposal, I’m not myself enthusiastic about any of these alternative terminologies.

But I personally do like the exception terminology. Most programmers coming to Rust will have extensively used languages that use this terminology. Of course, some of them hate those systems in those languages - that’s part of why many hobbyists chose to use Rust in the first place - but I am always designing for people who don’t have a choice about what language they use. For these people, I believe that understanding Rust’s try/throw behavior as relatively similar, syntactically, to whatever language they come from, but with the modification that fallible function calls must be annotated with ?, and with a value-type reification of fallibility into Result that try evaluates to, is a smaller pivot than starting from scratch with new keywords.

Of course this is a point of debate: is it similar enough that its a small pivot, or dissimilar enough that they will be confused by the way it behaves differently from their expectations? To take an existing example: I don’t think the differences in our async/await syntax from the behavior of languages like Python and JavaScript would justify choosing different keywords for the feature. But maybe here the trade off is different. This is the kind of discussion I am open to having, but usually these responses are some conflation between this and complaints about what users don’t like in the exception systems of other languages, properties that no proposal for Rust actually has.

This would make IDE integration worse

This argument is interesting, but the situation is no different from async/await and generators. Basically, yes, like all syntactic changes to Rust, IDE integration would need to handle it properly. Usually, we consider this as a cost, but not a particularly overwhelming one.

I’d be interested in learning if there are any particular challenges that make this seem more difficult for IDEs than other syntactic changes, the authors of rust-analyzer should feel free to get in touch with me if that’s the case.

std types should not be special cased

Adding lang items is always a cost, but a small one. It’s the reality today that std defines a large number of types which have special handling by the syntactic sugar and even the type system of the language. In some cases, we make special cased types open-ended with traits (and this is what the Try trait is supposed to be for), and it’d be great to support an open-ended set of “result-like” types. In particular, having some level of support for both Result and Option (even if Result is given some preference) seems like it should be at least a soft constraint for feature proposals.

But it’s pretty telling that we have still not stabilized the Try trait some 4 or 5 years after stabilizing ?. There just isn’t a lot of pressure from users to define their own Try types.

Obviously, only some problems justify having special syntactic integration between the language and std. But fallibility is not an uncommon problem, any more than iteration or asynchrony are. We have clear demonstration that it is one of the major issues users need to manage when writing code. We have clear precedent that we handle these effectful problems using a set of effectful syntactic sugar that naturally integrate with the language. Opposition to this is opposition to the way that Rust is designed already.

Conclusion

I’ll be honest, I don’t expect many people who have already staked out a position in opposition to this feature to be convinced by this blog post. I think with most of you I have differences of fundamental values and I can never convince you that this is a net positive. There are still a handful of people who hold to their orignal position that ? was a mistake, after all.

But if you feel open-minded that maybe you have not understood the value proposition of this feature, try the fehler crate in a real project and see if you can see the benefits of ok-wrapping when you write code.