Skip to content

Instantly share code, notes, and snippets.

@jnicklas
Created June 20, 2017 14:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnicklas/dbebe843fad1ccc0db4842f7bf7b308c to your computer and use it in GitHub Desktop.
Save jnicklas/dbebe843fad1ccc0db4842f7bf7b308c to your computer and use it in GitHub Desktop.
  • 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. 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 "unchecked 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment