5 ms·
I would love to see an article like this for error handling in Rust. I was really interested in using conditions, but those were apparently backed out. Which le
by Nycto 12y ago
I would love to see an article like this for error handling in Rust. I was really interested in using conditions, but those were apparently backed out. Which leaves error handling through return values and macros. This seems like a step back from Exceptions to me. I want to be convinced otherwise, but I'm struggling to see how this is better than other mechanisms.
- TheHydroImpulse 12y agoError handling in Rust is actually pretty awesome. There's a standard `Result<T, E>` type. One can write a potentially fail-able function like: fn can_fail(arg: bool) -> Result<(), StrBuf> { if arg { Ok(()) } else { Err(StrBuf::from_str("Oops! Something went wrong.")) } } Here, there's no value when the function succeeds `()`. When it fails (I don't mean fail as in a `fail!()` or a panic or anything), you get a string back. You can pattern match the return value: match can_fail(true) { Ok(_) => {}, Err(err) => {} } This can get quite cumbersome, however. That's why there's a `try!` macro that adds composability. The idea is that if you have a function returning a `Result`, wherever that function is being called could also return a `Result`. fn higher_up() -> Result<(), StrBuf> { try!(can_fail(true)); } `try!` is simply: match $e { Ok(e) => e, Err(e) => return Err(e) } This allows error to propagate up the chain. When you're working in big-ish projects, it'd be best to have something better than a simple `StrBuf` for an error. You'd probably want a struct: pub struct LibError { message: StrBuf, error: Error } pub enum Error { One, Two, Three } Where `Lib` is the library/project name. You can then create a new result type based on your new error type: type LibResult<T> = Result<T, LibError>; Then you can use `LibResult<T>` everywhere in your app. You can view this example done in a few of my own libraries (https://github.com/TheHydroImpulse/gossip.rs/blob/master/src/gossip/util.rs#L14 https://github.com/TheHydroImpulse/gossip.rs/blob/master/src...) and cargo (https://github.com/carlhuda/cargo/blob/master/src/cargo/util/result.rs https://github.com/carlhuda/cargo/blob/master/src/cargo/util...). That's a simple overview of it. Error handling is super simple, not verbose (thanks to try!) and in your control. Because of Rust's type system, things like `Result<T, E>` is available and are so much better than simple return values (like integers: -1 vs 0 uhhh)
- tomjakubowski 12y agoIn addition to the (very handy) try! macro you can also map over Results and even chain monadic-like operations on them (either on the Ok or Err sides of a Result): enum FooError { XWasFalse, XWasUnknown } enum BarError { VectorTooLarge, FooErr(FooError) } fn foo(x: Option<bool>) -> Result<uint, FooError> { match x { Some(true) => Ok(42), Some(false) => Err(XWasFalse), _ => Err(XWasUnknown) } } fn bar() -> Result<Vec<uint>, BarError> { foo(None).or_else(|e| { // We can recover from an XWasUnknown error returned by // Foo, but not from a XWasFalse, so we return the error wrapped // in bar's error type. match e { XWasFalse => Err(FooErr(e)), XWasUnknown => Ok(99) } }).and_then(|n| { if n < 100 { let vec: Vec<()> = Vec::with_capacity(n); Ok(vec) } else { Err(VectorTooLarge) } }).map(|vec| { vec.iter().map(|_| { 42 }).collect() }) } edit: added a better example
- doe88 12y agoAnother really great thing about returning a result Result<T, E> is that your caller code will then must use this return value or otherwise a warning will be emitted by the compiler at compile time. For those interested, more details are provided in the core lib documentation: http://doc.rust-lang.org/core/result/ http://doc.rust-lang.org/core/result/
- Pxtl 12y agoI tend to practice "only catch what you can handle" in exception-enabled languages - I haven't written in a systems language in almost a decade, mostly bad memories of C. How much does error-handling get in the way when you have to live without stack unwinding?
- kibwen 12y agoSo, normally in Rust, it's no problem to ignore the return value of a function. However, some types are tagged with the `#[must_use]` attribute, which makes it a warning at compile time to ignore the return value of any function that returns that type. Take the following program, which writes a buffer of bytes directly to stdout: fn main() { let mut out = std::io::stdout(); out.write([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]); } The `.write()` method returns a Result type. The output of compiling this program: $ rustc pxtl.rs pxtl.rs:3:5: 3:53 warning: unused result which must be used, #[warn(unused_must_use)] on by default pxtl.rs:3 out.write([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Again, it's just a warning, so the program will compile and run as expected: $ ./pxtl Hello! If you really don't care about the return value here, the simplest (and probably best) way of appeasing this warning is to explicitly ignore the return type by making use of pattern matching: fn main() { let mut out = std::io::stdout(); let _ = out.write([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]); } In Rust, the underscore is a pattern that means "I don't care about this thing, completely ignore it". The advantage of using the underscore here rather than an actual variable, e.g. `let x = out.write(...)`, is that it will be impossible to refer to the return value later on and thus explicitly expresses your intent to ignore it. (Furthermore, if you assigned the return value to a variable and then didn't use it later on, Rust would emit yet another warning, this time for having an unused variable.) The warning message alludes to a second way of silencing this error, which is by sticking the `#[allow(unused_must_use)]` attribute on top of your function. This will silence any warnings that arise from that function. If you wanted to disable this warning for your entire program, you could instead stick the `#![allow(unused_must_use)]` global attribute at the top of your program. Alternatively, you could compile the program with the `--allow unused_must_use` flag to completely silence all warnings of this type. (One final note: in all cases where you see word "allow" used above, if you replace it with "deny" it will turn the warning into a compile-time error, thus enabling you to enforce a more rigorous error-handling strategy if you so choose.)
- proksoup 12y agoIs it maybe explicit programming? In my own mind I don't understand why errors should receive special treatment in the language. I prefer to explicitly handle them always.
- kibwen 12y ago> This seems like a step back from Exceptions to me. I > want to be convinced otherwise, but I'm struggling to > see how this is better than other mechanisms. In a low-level language, guaranteeing memory safety in the face of resumable exceptions would be a nightmare. See Graydon's original post on the choice to avoid exceptions: https://mail.mozilla.org/pipermail/rust-dev/2013-April/003815.html https://mail.mozilla.org/pipermail/rust-dev/2013-April/00381... Selected quote: > In particular, to summarize for the impatient: once you get resumable > exceptions, your code can only be correct if it leaves every data > structure that might persist through an unwind-and-catch (that it > acquired through &mut or @mut or the like, and live in an outer frame) > in an internally-consistent state, at every possible exception-point. > I.e. you have to write in transactional/atomic-writes style in order to > be correct. This is both performance-punitive and very hard to get > right. Most C++ code simply isn't correct in this sense. Convince > yourself via a quick read through the GotWs strcat linked to: > http://www.gotw.ca/gotw/059.htm > http://www.gotw.ca/gotw/008.htm For more on the topic of exception-safety in C++, see the following paper by Bjarne Stroustrup: http://www.stroustrup.com/except.pdf http://www.stroustrup.com/except.pdf I don't think that Rust's error handling solution is ideal, but I think that it might be approaching the best possible solution for its chosen context. Error handling is a hard problem!
- kibwen 12y agoOne last thing that deserves to be mentioned: Rust does have unwinding-on-failure, which is similar to exceptions, with the restriction that unwinding can only be caught at task boundaries. This allows failure in a single component to be isolated and contained. The pertinent distinction here is that the unwinding is not resumable in the normal sense; at best, a parent task can detect that a child task has failed and attempt to restart the task, without having the ability to persist any of the failed task's state.
- shasta 12y ago> resumable exceptions The word resumable is important to that quote, which isn't arguing against exceptions in general.
- 12y ago