I respectfully disagree.
The assert!() macro is a way to document invariants. It's a bug if a variant is violated. It shouldn't have happened, but if it happens, then there's nothing the user can do except reporting the crash.
The unwrap() and expect() method document the invariant that the None and Err() variants shouldn't occur.
It's fail fast.
You should use error handling only if users would be able to handle the errors.
Tell the end user that the file they wanted to open is not readable, for example. Tell the users of your library that an error happened, like a parse error of a config file. And so on. Tell the users what they can fix themselves.
A bug in your library or program, that's something different. Fail fast! And Rust panics are perfect for that.
Using the assert macro in your code is (in my experience) generally bad. If your code is written well, you can never test that code path. Document invariants with tests instead, or better yet with infallible code.
A language has to use destructors to clean up for almost everything for this to work. "?" has no "catch" clause within the function. So if an object has an invariant, and that invariant must be restored on error, the destructors must restore the invariant. If that just means unlocking locks, closing files, or deleting data structures, that works. If some more complex invariant needs to be restored, "?" isn't enough. The calling function has to clean things up. This usually means encapsulating the function that does the work in a function that handles errors and cleanup. Basically, a try block.
There are certain obvious (and some less obvious) benefits to both exceptions and results, but I get the impression a lot of programmers have overreacted against exceptions.
Exceptions "just work" the same in every codebase and require little boilerplate in most languages. I think results really shine for internal business logic where errors are more "invalid" than "exceptional."
https://blog.verygoodsoftwarenotvirus.ru/posts/errors-in-go/
I read it. The first paragraph dismisses preferences in a matter that boils down to preference.
> Note that for any sufficiently complex program that invokes many dependencies, this stack trace will be so far down the chain that you may not even see where you’re making the call that causes it.
I don't understand this part. Why would my code not appear in the stack trace? Did this author know how to read stack traces?
With a stack trace, I have to cross-reference with code (ensuring versions match) and filter out a bunch of irrelevant calls in the stack. It’s not uncommon for the stack trace to end deep in library code with the root cause being many calls removed, making me check through a bunch of call sites to figure out what happened.
In Go if good context is added to errors, an error log is generally enough on its own to make it obvious exactly what went wrong.
Something that should be supported directly.
> Something that should be supported directly.
Rust has "adapted" some crates into stdlib in the past. Are there any efforts to that for error handling?
If Rust had adopted error_chain into the stdlib, that would have been a huge mistake.
That is the kind of excuse we give in C and C++ land, using static analysis isn't that bad.
Is Rust doing something different?
Rust is using the data structure, but doesn't have a way to express or use the monad, so you have to deal with and manually propagate the error. But like, if you do not have the monad, the data structure is just awkward... the only reason the data structure for this monad even exists at all is to support the monad, and the reason for the monad is to get a syntax similar to exceptions! In a language with a ton of hard-coded syntax for all of these things you'd use a monad for in Haskell--whether it's error handling, asynchronous execution, scope allocation... whatever floats your boat--you should just use exceptions.
Because it's just another type you could also do whatever else you like, unlike with the Exceptions in typical languages which have them where too bad, we bolted the information to the control flow so now we're going on a journey.
If you want to bolt control flow to some information in Rust that's fine, feel free to define a function which returns ControlFlow::Break for success if that suits you, the try operator understands what you meant, early success is fine. Actually you can see this reflected in the larger language because break 'label value; exists in Rust unlike for example C++.
.site-page.loading { opacity: unset; }One of the biggest problems with Rust error handling is that if you want to have explicit error return types encoded in the type system you need to create ad hoc enums for each method that returns an error. If you only use a single error type for all functions you will inevitably have functions returning Results that contain error variants that the function will never actually return but still need to be handled.
Without enum variants being explicit types and the ability to easily create anonymous enums using union and intersection operators Rust enums require a ton of boilerplate.
For example, I have a function that takes an array of bytes, decodes it as UTF-8 to text, parses that text into an i32, and checks the int that it is within a valid range. This is not a big function. But it might produce one of: 1. str::Utf8Error, 2. num::ParseIntError, or 3. MyCustomInBoundsError. There's no clean way to write a Rust function that could return either of them. I had to bundle everything up into an enum and then return that, and then the caller has to do some "match" acrobatics to handle each error differently.
I hate to say this, but I miss Python and C++'s exceptions. How nice to just try: something and then:
except SomeError:
doFoo()
except ThatErrror:
doBar()
except AnotherError:
doBaz()
finally:
sayGoodbye()
An elegant weapon for a more civilized age.What do I know though? I'm still in the larval stage of Rust learning where I'm randomly adding &, *, .deref() and .clone() just to try to get the compiler to accept my code.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
fn main() {
match process(&[0x34, 0x32]) {
Ok(n) => println!("{n} is the meaning of life"),
Err(e) => {
if e.is::<std::str::Utf8Error>() {
eprintln!("Failed to decode: {e}");
} else if e.is::<std::num::ParseIntError>() {
eprintln!("Failed to parse: {e}");
} else {
eprintln!("{e}");
}
}
}
}
fn process(bytes: &[u8]) -> Result<i32, Box<dyn std::error::Error>> {
let s = std::str::from_utf8(bytes)?;
let n = s.parse()?;
if n > 10 {
return Err(format!("{n} is out of bounds").into())
}
Ok(n)
}
In library code though that would make it generally more difficult to use the library, so the enum approach is more idiomatic. Then that comes out as match(e) {
MyError::Decode(e) => { ... }
MyError::ParseInt(e) => { ... }
...
}
etc, which is isomorphic to the style you miss. What you're perhaps missing the is that `except ...` is the just a language keyword to match on types, but that Rust prefers to encode type information as values, so that keyword just isn't needed.I feel you on the larval stage. Once you get past that, Rust starts to make a lot of sense.
An example: https://play.rust-lang.org/?version=stable&mode=debug&editio...
Then people who don’t want to engage with ThisError and Anyhow do bullshit hacks like making everything a string that has to be parsed (and don’t provide functions to parse).
I get why it is that way, but it feels icky.
There is no reason it has to be so boilerplate heavy, a lot of it can be fixed but someone has to put in the work. The only technical reason that could hold it back is compile times.