Pre-RFC: Throwing Functions

  • Feature Name: throwing_functions
  • Start Date: 2017-06-20
  • RFC PR: (leave this empty)
  • Rust Issue: (leave this empty)

Summary

Add an annotation to functions that they may “throw” an error type, which changes the function’s return type to Result<T, E> where T is the functions return type and E is the function’s annotated error type. Also add a macro throw! to facilitate early return of errors.

Motivation

Rust’s error handling is on the one hand very powerful, and values correctness and clarity, on the other hand it can be somewhat cumbersome and unergonomic in some cases. This is why error handling has been singled out as part of the ergonomics initiative.

The introduction of the ? operator and the Try trait has made it easier to write functions which propagate errors. With RFC1937, the ? operator will become even more prevalent.

One downside with the ? operator is that the positive case, when no error has occurred, still necessitates special handling, since the return value needs to be wrapped in Ok(). This is especially cumbersome for functions which return (), since instead of simply not specifying a return value, the unsightly Ok(()) needs to be returned. This is not only aesthetically unfortunate, but also confusing to new users.

Here’s an example of the necessity of wrapping the return value in Ok and the Ok(()) return value in particular:

use std::path::Path;
use std::io::{self, Read};
use std::fs::File;

fn read_file(path: &Path) -> Result<String, io::Error> {
  let mut buffer = String::new();
  let file = File::open(path)?;
  file.read_to_string(&mut buffer)?;
  Ok(buffer)
}

fn main() -> Result<(), io::Error> {
  let content = read_file(&Path::new("test.txt"))?;
  println!("{}", content);
  Ok(())
}

Detailed design

The syntax of function definitions, both for free functions, inherent impls, and functions in trait definitions and implementations will be extended with the throws contextual keyword and a type, which must appear after the function’s return type and before any where clauses.

The return type of any function which is annotated with throws becomes Result<T, E> where T is the function’s return type and E is the error type which appears after the throws keyword.

The function body must evaluate to T and any early return from the function must also evaluate to T. It is not possible to return a Result<T, E> from the function body. If the ? is used within such a function, unless it is used within a catch block, it will operate in the same way as if it were used in a regular function with return type Result<T, E>.

For example:

use std::path::Path;
use std::io::{self, Read};
use std::fs::File;

fn read_file(path: &Path) -> String throws io::Error {
  let mut buffer = String::new();
  let file = File::open(path)?;
  file.read_to_string(&mut buffer)?;
  buffer
}

fn main() throws io::Error {
  let content = read_file(&Path::new("test.txt"))?;
  println!("{}", content);
}

Forcing an error return

One particular case where this design is till somewhat cumbersome is in returning an error from a function directly, this can be solved by adding a throw!($expr) macro to the prelude, which expands to Err($expr)?. While it would be nicer to have a throw keyword, similar to return, since throw is currently not a reserved keyword, this would be a breaking change, and probably not acceptable in Rust < 2.0.

For example,

fn main() throws MyError {
  let content = read_file(&Path::new("test.txt"))?;
  match &content {
    "ok" => println!("Everything is ok!"),
    _ => throw!(MyError::InvalidFileContent),
  };
};

How We Teach This

This feature should be taught as part of Rust’s error handling story, in particular since it is closely tied to the ? operator, it should be taught alongside it. In particular the connection between this and the Result type should be made explicit. It is important to emphasize that this is not an exception mechanism and that thrown errors do not automatically propagate up the callstack, like it might be expected from other other languages, and as is the case with panics.

A major benefit of this feature is that in a pre-rigorous stage, users can be effective in handling errors in Rust through the ? operator, the throws annotation and the throw! macro, even if they are not familiar with algebraic data types or Result return types.

Drawbacks

The major downside of this feature is that it makes the function signature somewhat more opaque. It is not immediately clear that the return value of the function is a Result, in fact the Result type does not appear in the function signature at all. This can appear this feature appear to be somewhat magical. It may hide the affect that other features of the Result type, such as monadic combinators or its implementations for FromIterator may be used with throwing functions.

Alternatives

  • An alternative design would be to automatically coerce the return value of any function which returns a Result into a Result::Ok value unless it is already a result. While this makes the type of the function more clear, it introduces a lot of ambiguity around the return value of the function. For example, if the return value of the function is Result<Result<T, E>, E>, what would a return value of Err(foo) mean?

  • Use a keyword for throw instead of a macro. While this would be aesthetically preferrable, it is probably not possible due to backward compatibility issues.

  • Skip the throw! macro all together. This would mean that users would still have to learn about the Err variant constructor in the pre-rigorous stage.

  • Do nothing, which leaves the status quo of the somewhat unsightly Ok(()) and friends.

Unresolved questions

  • What is the appropriate name for this feature? This has previously been called “checked exceptions”, but this terminology brings with it a lot of baggage, and also is not correct, since this is in fact not an exception mechanism.

  • Should this mechanism somehow be extended to other types which can be used with the ? operator?

  • This feature does not have a strightforward desugaring to Rust syntax, like the ? operator does for example, are there any challenges with implementing this design?

2 Likes

There’s some issue with the markdown formatting here, it renders fine both locally and on GitHub, and I can’t see anything that’s wrong.

EDIT: never mind, seems like Discuss doesn’t like fenced code blocks.

I’d definitely would like to see a solution for functions returning Ok(()).

However, I’m worried that calling it throwing (and using syntax similar to Java’s checked exceptions) can give wrong expectations to users coming to Rust (e.g. the returned value is still a Result, and “throwing” doesn’t unwind the stack unpredictably).

Result as an explicit function return type is not too bad. It’s explicit. It can be aliased to avoid typing complex types repeatedly.

13 Likes

I agree that throw and throws is a bit problematic, since it implies that something should be caught, which isn’t really the case here. On the other hand, Rust is already going down this road somewhat with the planned addition of catch. I tried to come up with a less overloaded term, but I was unable to come up with anything else which seemed reasonable.

I think the major drawback is adding new syntax just for the convenience of omitting Ok() in returns, which may or may not be worth it.

There’s also an alternative syntax:

ok fn read_file(path: &Path) -> Result<String, io::Error> {

where “ok” (could bikeshed this to some other string) enables the Ok desugaring.

The advantage of this syntax is that the return type is explicit and it parallels the async function syntax assuming the C#/TypeScript syntax is adopted in Rust (Future and Result are both monads and thus async/await and ok/? are analogous), the disadvantage is that it is less similar to the C++/Java throw declaration syntax.

Which also generates another advantage, it works with all types that implement Try. Basically it would implicitly wrap any returned value via Try::from_ok. That to me seems a much more useful route to go than forcing it to be Result specific.

2 Likes

We already intended to do auto-wrapping for catch blocks (#41414). This alternative is a more natural extension than the main Pre-RFC.

I'd expect Err(foo) returns the outer Result, since you could always use Ok(Err(foo)) to make the inner Result.

1 Like

When would it actually make sense to do this? In other words, is this a scenario we should actually worry about? (I recognize that the language has to do something, but if we don't think it will come up, it could just do whatever is convenient.)

For just auto-wrapped returns it may be possible to require no new syntax at all. Currently returning a result value without an Ok/Err wrapper is a type error, so allowing auto-wrapping under a condition that the value wouldn’t type-check otherwise should be perfectly backwards-compatible.

e.g.

fn foo<T,E>() -> Result<T,E> {
    …
   return x;
}

It could work something like that:

  1. Is type of x a Result<T,E>? Return x as-is.
  2. Is type of x = T, but not E? Return Ok(x)
  3. Is type of x = E, but not T? Return Err(x)
  4. Error - ambiguous (could be either T or E, e.g. -> Result<u32, u32> & return 1)
9 Likes

As far as I can see it, if we want to get rid of Ok(()), there are four possible solutions:

  1. A special handling of Result in the compiler where values are coerced if possible (as proposed by @kornel)

  2. A trait based solution, maybe using From::from to implicitly coerce returned values. It could use a specific trait for this, let’s say ReturnFrom or something.

  3. Annotate the function somehow making it explicit that a conversion will happen, but keep the return type as Result<T, E>

  4. Annotate the function with a separate error type (this pre-RFC proposal)


I find (1) problematic, because it is unexpected that this would work in the first place. Why would returning the wrong type from a function suddenly work? And why only for certain functions? I also find the coherence issue extremely problematic. Remember that it’s not enough that T and E aren’t identical, but it must be the case that they couldn’t be identical. For example, the following would work:

fn foo() -> Result<&'static str, u32> {
    "test"
}

But the following wouldn’t:

fn foo<T>() -> Result<T, u32> {
    T::blah() // returns `T`
}

Because the compiler cannot be sure that T isn’t u32 and in that case the return value of the function would be ambiguous.

I think this would lead to very unintuitive behaviour where this feature sometimes works, due to reasons which aren’t very intuitive.


The second solution (2) is problematic because of the same coherence reasons as (1). It is also not feasible to ever stabilize this trait, since it would open up the language to all kinds of implicit coercions, which could cause all kinds of coherence problems, and implicit behaviour that users are not expecting. I’m not sure, but it also seems problematic to me to even write such a trait and a sensible implementation for Result, given today’s coherence rules. I think such a trait would require trait bound inequalities, which we don’t currently have, and which are not even anywhere near being implemented.

Personally I find both of these options far more magical than using an annotation.


I think both (3) and (4) have their pros and cons.

With either of these solutions, if we want to avoid the coherence problems of (1) and (2), it should not be possible to return a Result<T, E> type, instead the unwrapped T type must be returned, with E only being propagated via the ? operator.

It’s notable that C# and TypeScript, which are statically typed languages that implement async/await which is superficially similar to this feature, have both chosen the first option. They have both an async annotation on the function, yet the return type is Task<T> or Promise<T> respectively. So from a prior-art perspective, option (3) seems to be more common.

However, I personally find (4) much more attractive. I think being able to omit Result is actually an advantage as I laid out in the pre-RFC text.

I really like the fact that when transitioning from fn foo() -> T to fn foo() -> T throws E the type after -> is always what the function body evaluates to. It feels like the wrapping that is happening is much less magical and odd that way.

I’ve also been unable to come up with a syntax for (3) which I find acceptable. The suggestion above to use ok fn foo() seems like it could be problematic to parse to me, since ok is not a reserved keyword, but I could be wrong, I don’t really know how the parser works. I also find it a bit too subtle. Since this annotation has a rather profound effect on the function body. It also seems quite disconnected from the return type of the function, which is what the annotation affects, so it feels like it should be visually closer to it.

3 Likes

The throws syntax possibility was mentioned back in https://github.com/rust-lang/rfcs/blob/master/text/0243-trait-based-exception-handling.md#throw-and-throws. I’m not a big fan, since it seems to disadvantage Option and Poll.

The current direction from the lang team here is that function and catch results should automatically be wrapped using Try::from_ok if they’re not already the correct type. That way changing from -> T to -> io::Result<T> will “just work”*. I’m behind on writing the RFC, sorry :disappointed: (Personally I’ve stopped calling this a coercion, since it’s location-specific and non-transitive, unlike the existing coercions.)

My latest strawman for “mark the functions where the different behaviour is desired” syntax is fn blah()? -> Option<i32> { 4 }, trying to mirror how the call (let x = blah()?;) ends up looking. I’m torn; explicit marking makes some things easier, but not having to do anything would be awfully nice.

I would like to get a throw e; syntax at some point, but near term it’ll probably be spelled Err(e)?. Adding keywords is hard right now, as throw(4) is a valid function call that we can’t break.

​* Modulo producing return values via methods on traits left to be inferred, like Default:default()

2 Likes

To me, this seems like one of those ergonomics issues where the problem is not how many characters you need to type or where you need to type them, but the fact that you need to remember to type anything at all. So I don’t understand any of the suggestions that we replace ‘Ok()’ at the “return site” with some other annotation like an ‘ok’ or ‘?’ or ‘throws’ in the function signature. Not only does that not solve the problem, but it’s introducing multiple equally good ways of writing the same thing, adding unnecessary implementation details to the function’s signature, and in the case of ‘throws’ potentially misleading a ton of newcomers from other languages.

When I originally heard about this problem, it sounded like it was mostly about cases like ‘Ok(())’ where you’re “returning nothing”. For that case we could potentially have a special case for “empty” types like (). But now it sounds like ‘Ok(buffer)’ is also part of the concern, in which case it seems obvious that the only feasible solution that might actually solve this problem without creating new equally bad problems is “if a return value doesn’t type check, try adding Ok() before giving up”. So +1 for using Try::from_ok.

10 Likes

There's no ambiguity in this case, because Rust doesn't do SFINAE. "T that may be u32" and u32 are always distinct types in Rust already:

| t | ^ expected u32, found type parameter | = note: expected type u32 found type T

From a PR point of view, I find that spelling Ok(()) in functions which return Result<(),E> is actually a good thing. It reads as: “and if we get here, then everything has worked ok”, which helps reinforce the notion that errors have been thought about and treated, and helps distinguish Rust from the perceived problems of exception-based solutions. I for one, am proud of our glorious Ok(()).

My counter-proposal is to simply define :ok_hand: as an alias for Ok(()).

I also don’t see any of the proposals as a win for teaching Rust. For instance, consider a case where a bunch of Result<T,E> gets stored in a data structure instead of T values, ie where a non-monadic function like map() has been used instead of a monadic one. In this case, I think it is easier to make sense of this (failing) example than what you get with throws.

fn faillible_seventeen(i: u32) -> Result<u32, SomeErrorType> {
    Ok(17)
}

fn main() {
    let v = vec![1,2,3,4];
    let vv = v.map(faillible_seventeen);
    assert_eq!(vv, [17,17,17,17]); // This won't type-check.
    println!(success)
}

With throws, the code looks like the following, and it is harder to grasp the situation with the spurious Ok() around each 17.

fn faillible_seventeen(i: u32) -> u32 throws SomeErrorType {
    17
}

fn main() {
    let v = vec![1,2,3,4];
    let vv = v.map(faillible_seventeen);
    assert_eq!(vv, [17,17,17,17]); // This won't type-check
    println!(success)
}

In particular, the second version gives the false impression that the following is not possible:

fn main() {
    let v = vec![1,2,3,4];
    let vv = v.flat_map(faillible_seventeen); // We keep the successful seventeens
    assert_eq!(vv, [17,17,17,17]); 
    println!(success)
}
15 Likes

An example of the sort of function in which (I believe) Ok(()) is just pure noise:

fn write_multihash(w: &mut io::Write, multihash: &MultiHash) -> io::Result<()> {
    w.write_usize_varint(multihash.code())?;
    w.write_usize_varint(multihash.len())?;
    w.write_all(multihash.digest())?;
    Ok(())
}

I also find this example really bad because there is an alternative way to write it and I’m like 70:30 on which way is better

fn write_multihash(w: &mut io::Write, multihash: &MultiHash) -> io::Result<()> {
    w.write_usize_varint(multihash.code())?;
    w.write_usize_varint(multihash.len())?;
    w.write_all(multihash.digest())
}

I’d really love to be able to write this as if it were a normal return-value-less function and have the Ok(()) implied (the first example minus the last line).

2 Likes

I always have been glad that Rust didn’t have special operators for stuff like this, but instead everything was a convention instead. Similar for constructors.

However, I hugely dislike converting X into Ok(X) or into Ok(Some(X)) without any notice, as there is this popular pattern of fn foo() -> Result<(),()> { bar() }. With the proposed implicit coercion, that would be ambiguous, bar could return either a Result<(),()>, or (). I would really really not like such ambiguities to appear in the language, that seems like a big bug to me. Also, unless you make the coercion work on function bodies only (which is a novelty, and would make the language harder to learn) is this ugliness with that you now have to allow stuff like let x: Option<Option<i32>> = 4;.

Instead I would prefer having some explicit syntax, where we opt in to a conversion, and where the fn foo() -> Result<T,E> { bar() } case above is forbidden (because it expects either T or E but not the Result). Which syntax we end up, whether its fn blah()? -> Option<i32> like @scottmcm proposed, or the throws syntax, I don’t really care, as long as its explicit and the conversion is a must-do one.

16 Likes

I agree completely. The function body should evaluate to T or to Result<T, E>, for both to be allowed would be very confusing.

3 Likes

@est31 Thanks for substantiating the feeling I’ve had that something’s fishy with the "just coerce to Ok" approach, but couldn’t articulate :slight_smile:

I think the idea to use ? instead of throws in the signature would make sense if we left off the "Try type" in the same way as the throws proposal does. Because in that case, given fn blah()? -> i32 { 42 }, the intuition would be that the type of blah()? as an expression is precisely i32, which is kinda neat. But it unfortunately doesn’t seem like it can work, because it doesn’t leave anywhere to specify what the error type is.

Other things:

  • Someone suggested that a drawback of throws is that it would only work with Result and not other Try types, but I don’t think this has to be the case. In principle, the body of a throws function would never mention the variants of the Try type it’s returning, they would only be created through ? for errors and wrapping for ‘successes’. So in theory it could return any Try type we choose; but even if it returns Result specifically, Try itself allows it to be converted to a different one. If I’m not missing something, then fn foo() -> X throws Y could desugar into any of fn foo() -> Result<X, Y>, fn foo() -> impl Try<Ok=X, Err=Y> (in which case the only thing the caller could do with it is ? or .try()), or fn foo<Res: Try<Ok=X, Err=Y>() -> Res (in which case foo() would desugar to use Try::from_ok() and Try::from_err() at its exit points to convert to whichever type the caller requests), and they all seem like workable approaches (though I haven’t worked it out in detail). I’m not sure which one might be best.

  • This is of course related to how catch should work. I’m of the opinion that catch should just always Ok-wrap (or Some-wrap, etc.) its result, as in the original RFC, but I haven’t had time to read the GitHub thread about this.

  • This also seems related to the design work around async/await, where #[async] would implicitly wrap a function’s written return type in a Future in a similar way to how throws would wrap it in a Result or Try.

3 Likes
fn foo() -> X throws Y     ==>     fn foo() -> impl Try<Ok=X, Err=Y>

That is an interesting variant that I didn’t consider at all. I actually quite like it.


I’m also on the wagon that for a given function it should either always wrap its return values (whether an implicit or explicit return) in a Try::Ok variant, or never wrap them. Using the throws syntax and given fn bar() -> Result<(), ()> that would mean either of

fn foo() -> Result<(), ()> { bar() }
fn foo() -> () throws () { bar()? }

would work, but

fn foo() -> () throws () { bar() }

would give something like

error[E0308]: mismatched types
  |
2 | fn foo() -> () throws () { bar() }
  |                            ^^^^^ expected (), found enum `std::result::Result`
  |
  = note: expected type `()`
             found type `std::result::Result<(), ()>`

possibly with some help text more specific to throws functions than the normal help.

One downside mentioned in the OP that I haven’t seen discussed is that it means you can’t use a normal return to return a Try::Err variant from the function. Is there any way to make that more ergonomic than having a macro expanding to Err(...)? ?

3 Likes

Why don’t you just write

fn foo() -> Result<()> { catch {
    stuff_that_can_fail()?;
    stuff_that_cannot();
    // no Ok(()) necessary
}}
1 Like