Hacker News new | past | comments | ask | show | jobs | submit login
Things I Enjoy in Rust: Error Handling (jonstodle.com)
118 points by ingve 5 months ago | hide | past | web | favorite | 85 comments



It's interesting to see modern languages tackling checked error handling. Particularly in the light that Java's checked exceptions are widely considered to have been a failure. Yet I wonder if Java had better support for things like union types etc. if it would have turned out differently. After all, it did offer at least potential to achieve what everyone is after: you can write a piece of code and know for sure that you handled every possible type of error.

The most painful aspect of Java's (original) exceptions was how the only mechanism to handle multiple errors as a single case was using inheritance in the exception hierarchy. The result was that they propagated through interfaces and broke downstream code and it became pretty much impossible to design and maintain a stable API without wrapping exceptions excessively which in turn destroys the utility of the exception hierarchy, making the whole exercise futile.

The article seems a little superficial though. Immediately exiting with 'panic' does not seem like a real error handling approach for any realistic application. It doesn't show how you could handle different types of errors (eg: file not found vs I/O error reading file) with different cases. The case it does present is easily handled elegantly by nearly any error handling design.


I think the biggest problem with Java exception specs is that they're not composable. In other words, I can't define a method X that calls another method Y - potentially via dynamic dispatch - and has an exception specification of "whatever Y throws, plus E".

This manifests almost immediately. For example, suppose you wanted to implement a key/value database with disk storage. The logical core interface for it would be java.util.Map, except for the fact that its methods are declared as non-throwing; thus, you cannot propagate IOException from the underlying storage layer, unless you wrap it into unchecked RuntimeException.

Conversely, java.lang.Appendable is a very generic interface for appending characters or sequences of characters to something. But because its designers knew that some of its implementations would be backed by I/O, all mutating methods in that interface are declared as throwing IOException. Thus, any generic code that tries to work with Appendable has to always assue that it can throw, and has to declare itself as throwing the same exception to propagate. And now the user of that generic code passes a no-throw implementation of Appendable to it, but still has to "handle" IOException that they know will never actually happen.

Or come up with some convoluted scheme, like java.util.Formatter does. This one wraps Appendable, but swallows all I/O exceptions to avoid having to propagate them in the most common case, where the wrapped Appendable is a StringBuilder. But since there are other possibilities, it still has to expose some API to examine such exceptions, which it does by providing a method on Formatter that returns the last swallowed exception that was thrown by Appendable. Of course, now you have to remember to actually check it, and you will silently get an incorrect result if you do not, which is worse than unchecked exceptions in the same scenario.

This all gets especially bad one you start working with HOFs, because their contracts are largely defined in terms of the behavior of functions that are passed to them. The old Java lambda proposal by Neal Gafter noted this, and proposed extending the syntax for generics to cover exception specifications (via union typing, indeed). In the end, they added UncheckedIOException to the standard library, specifically so that stuff like Stream.map() can be implemented on top of files, and propagate exceptions, without declaring them.


That is a very nice summary!

Some of these tensions seem unresolvable though. At some level if I want to toss a Map around everywhere and be able to transparently back it with a database, I'm going to have to accept that operations might fail. I can't have it both ways. In the end, the abstraction is "leaky". I don't really see how Rust or anything else can make that go away.

I feel like the closest I can get to have my cake and eat it too is by having the compiler transparently infer all the exceptions / errors that can be thrown by the calls I am making (like type inference for variables), but leave them out of the type signatures. Then I can opt in to enforcing the checking of them when I want to, so something like

    check {
       doSomething()
    }
    catch(...) { // every error now has to be handled
        ....
    }
I guess this is something like what Rust is achieving through the ? operator.


In the case of Map, since some maps fail and others don't, and those that fail can fail with different errors, it would be reasonable to express in in the type itself - i.e. instead of Map<T>, you have Map<T, E> (and then you also need union types to properly express E being a list of exceptions, and a unit type to express an empty list). Then any code that handles generic maps can also be generic wrt E, propagating whatever is thrown, and possibly adding its own errors, or catching some specific ones. And, conversely, you could only accept Map<T, Unit> for code that expects non-throwing maps, and then you don't need to catch or propagate anything in that code. Add defaulted generic parameters for convenience - e.g. Map<T, E=Unit> - and now API clients don't have to care about E if they don't want to, and are happy with things as they are.

Alternatively, you can just say that map is something that never throws, by definition. But, of course, that reduces the utility of the interface, by making it less generic than it could be. In some cases - especially with HOFs - the reduction in utility can be so substantial that it negates any gains from code reuse.


In rust, what you can do is define an error type as an enum, which holds the various kind of inner errors. Then you’re just a From implementation (which is very simple and macros exist to generate it) away from ? working on any of them, and it composes them together.


I've shown Rust's Result type to folks on occasion because I like it so much. The other aspect I like is the `?` operator, which allows for easier error propagation.

https://doc.rust-lang.org/book/ch09-02-recoverable-errors-wi...


I think propagating null operators, `?`, etc., all become a bit less interesting when you consider that mostly they're just specific implementations of a monadic interface and they could all just be represented like that instead. It doesn't seem sensible to me to try to "fake" monads everywhere and add different operators for different ones when we could just have one interface that describes short-circuiting based on the data type.


It's just not that simple: https://twitter.com/withoutboats/status/1027702531361857536?...

Rust doesn't have higher kinded types, every closure has a unique type that implements up to three different traits, and it has imperative control flow structures that don't exist in Haskell, et al.

By the time you've solved all these problems, you've moved away from monads to full-blown delimited continuations, one small step away from an effect system. And nobody really knows how to make that work while still providing Rust/C++-like "zero cost abstraction."

It might be nice, but it's not at all clear that it would even be "just one interface" anyway.


I'm not really talking about what's interesting or cool given a comparatively bad starting position, I'm talking about these language features in their own right. The idea of making special-case operators for what are just monads becomes less interesting in general, regardless where you started. It's certainly neat and practically useful, but it's a symptom more than it's a long-term solution.

This wasn't directed at Rust in particular; many languages have these kinds of operators and even more are trying to propose them and I wonder if people just don't see the big picture and/or don't care. You can have a good and productive language without seeing the big picture or maybe realizing too late, but it's not a paragon of good design. It's definitely a design smell to introduce special-cased operators for these things.

With that said, I really like Rust and I think it's an excellent language. This particular thing irks me because a lot of the people who'd exalt this kind of feature usually simultaneously argue that monads are somehow not relevant, while not realizing it's contradictory.


Rust isn't "a comparatively bad starting position." Those obstacles I listed are not just cases of "d'oh, if only we had thought of this earlier." They're conscious, intentional design choices to trade off a general monadic interface (and other similar designs) for real benefits in Rust's domain.

The people who made these choices, and the people who "exalt" them, thus cannot fairly be described as "just don't see the big picture," "don't care," or "not realizing it's contradictory." And until someone has figured out a way to combine a general monadic interface (or more likely, some other method for orchestrating continuations) with Rust-like zero-cost abstractions, these features continue to be "cool" "in their own right." :)


> Rust isn't "a comparatively bad starting position."

From the standpoint of "How do we handle things that behave like these things (monads) in a general way?" it's certainly not a good one. It's not that I'm saying that Rust is badly designed overall, but like other languages with these operators it exhibits this particular design smell. It's not really debatable that this kind of special-casing is bad design and lack of foresight or a general lack of caring about this thing in particular.

It's also fine not to care about it, just as it's fine to not care about generics if one so chooses.

> The people who made these choices, and the people who "exalt" them, thus cannot fairly be described as "just don't see the big picture," "don't care," or "not realizing it's contradictory." And until someone has figured out a way to combine a general monadic interface (or more likely, some other method for orchestrating continuations) with Rust-like zero-cost abstractions, these features continue to be "cool" "in their own right." :)

I think you're reading a bit more into this than I intended and I guess that's fair. I don't think it's that big a deal that Rust lacks a way to generalize over this concept, but let's not pretend it's a good thing. At which nth special operator for some variant of this behavior would you consider this to be generally not a great design choice? It's either some N or where you intentionally limit this simply because making a bunch of operators is a silly proposition. Either of those cases mean a failure in design. Is it a major one that matters to everyone? No, but it's a shortcoming.

It's fine for languages to make trade-offs and not be perfect in every way, I don't think it's terribly productive to not see obvious shortcomings just because we like something.


> I think you're reading a bit more into this than I intended and I guess that's fair.

That's what I'm reacting to, mostly. But also to the idea that it's some kind of universal, uncontroversial positive for a language to abstract over an unbounded number of implementations of monads.

In a tautological sense, of course, if a language doesn't support feature X that's a problem for people who want to use feature X. But we don't write programs to use language features- we write programs to solve problems.

That is, one could argue that feature X has too much complexity to be worth having in a language, and that solving the same problems another way (or not at all) is better. This is different from the point I made up-thread, and I'm still not trying to make it about monads. But it does seem to me that classifying every tradeoff as a shortcoming presupposes that if a language could support every single possible feature it would be perfect, which is also a silly proposition.


I think I understand the case you're making - that generic solutions are generally preferable to special-case solutions. That's a sensible enough position to take.

But it's pretty unreasonable to call it _bad design_ in a vacuum, as if there are no obstacles to a more generic design. The reason something like do-notation wasn't chosen is that nobody figured out a way to implement it without major trade-offs. So the alternative you're championing here either isn't possible within the constraints of the language, or the solution hasn't been discovered yet. The `?` operator was a pragmatic compromise.

I'm with you 100% that it is a shortcoming, but you can't really call it bad design given the reality of the situation. Compromises aren't bad design.


`?` isn't really that great - for all but toy cases you have to jump through hoops - especially if you are trying to handle different error types from separate in a unified way.

Not a fan of Result, either - it ties together the error type and the type of the desired item - these should be separate concerns.


> Not a fan of Result, either - it ties together the error type and the type of the desired item - these should be separate concerns.

How so? 'Result' is just a generic variant record with two variant cases, for the "success" and "error" condition. It's the exact same as the `value, err` pattern in Go, except that it additionally enforces proper semantics and handling of the outcome.


> `?` isn't really that great - for all but toy cases you have to jump through hoops - especially if you are trying to handle different error types from separate in a unified way.

To be clear, that isn't the fault of `?` but that Rust "requires" you to specify your error type. You could just specify `Box<Error>` and go about your day without boiler plate.

What you mention and my workaround are two extremes of the solution space. There are Rust crates inbetween that help people handle errors the way they need with minimal boiler plate.


One of the more interesting semi-recent additions to stable rust is the question mark operator. It's not covered in this Rust Error Handling article, but it's got some of the early-exit appeal of exceptions without too much unwind magic.

Unfortunately I haven't been able to get the question mark operator to work yet with my code, but it looks appealing. It's not 100% obvious to me what qualities my Result type needs in order to be able to make it work. Or whether it's bound in practice to the Result type(s) defined in std. I'm sure those details are all in the docs somewhere but I spent the time to look yet.


One thing you may be missing is that the `Result::Err` in your return type must implement `From` for the `Result::Err` type where you're using the `?` operator. That's already covered if those `Err` types are identical (`From<T> for T`), otherwise you need to implement the conversion.

    impl From<QuestionError> for ReturnError {
        fn from(error: QuestionError) -> ReturnError {
            // TODO
            unimplemented!()
        }
    }


Oh yeah, that's likely it. Thanks!

It it safe to just put a noop in there? What's From-ness? Is QuestionError defined in std? Where do I read more?

EDIT: oh I see, I didn't read carefully. I just need to provide a conversion. Your example is very clear.


It's how you explicitly cast from one type to another. To use the ? operator with a custom error type, it needs to be converted to the std error type.


It needs to be converted to whatever error type the function returns. That can be anything.


Also, if you use Box<Error> or failure[1]'s Error type you can get around implementing most of the conversion Traits, but you lose the ability to tell which errors can happen by just looking at the type signature and doing exhaustive matching on them.

[1]: https://github.com/withoutboats/failure


The ? operator is tied to std::ops::Try, and std::convert::From. https://doc.rust-lang.org/std/ops/trait.Try.html https://doc.rust-lang.org/std/convert/trait.From.html


Oh, hey, this was great (the link to the 'From' trait). "enum CliError" -- this example was crystal clear and answered a lot of my questions.


If you can post your code, we can help you out. One requirement, is that your function returns a Result<Ok, Err>. The Err needs to implement the same type for all the type returned.

This means you can use the "?" operator for functions that you did not create (libraries) but you'll also have to convert their Error type to your Error type.


It's worth reading through the doc page. It's not not too long: https://doc.rust-lang.org/stable/book/ch09-02-recoverable-er...


I had read that doc page but IMO it's not very clear about how you should use Result types. It's great for showing how you might want to use the question mark with libstd but IMO less clear about writing your own functions and Result<> types (though apparently the most interesting constraint seems to be regarding your Result's E type).

Someone elsewhere in this thread linked to an example of `impl From<io::Error>` and IMO that's a clear, explicit example of how to do it.


I'm kind of contemplating a career switch since in mobile JavaScript cross-platform seems to become pretty big and I just don't feel very warmly about becoming a JavaScript specialist.

What are the possibilities for somebody that's pretty heavy iOS senior to transfer to a role building applications or services in Rust (or Elixir, for that matter)?


AFAICT, 99% of the Rust jobs out there are just crypto startups. So at this point, not that great.


I've noticed that Rust projects are starting to pop up at Microsoft. As of this writing, there are 5 open SDE job positions that mention Rust: https://careers.microsoft.com/us/en/search-results?rt=profes...


Here at Facebook they are starting to pop up as well: https://www.facebook.com/careers/jobs?q=rust


We are an information security company in Europe using Rust for performance-critical parts of our Backend.

Of course, that's just one anecdotal example, but I'd encourage you, dep_b, to look for openings. I'd guess that only very few companies use Rust exclusively right now, because of its relatively young age and because it frankly doesn't make sense everywhere, but many use another main language plus Rust.


There is a company in Texas doing power grid /energy analysis with Rust. A company in Denmark doing non crypto stuff that I interviewed with a while back. Microsoft is using it a little, Cloudfare is using it a little.


Can you disclose what the energy company in Texas is?


Any job is a Rust job if you're in charge of deciding the language.


It's a tough sell to manglement considering there's almost no developers who know it, and it's not something you can ramp up on quickly like Java to C# or Ruby to Python.


I remember that in the past, early 00s, this was true for Python as well.

One had to make an active effort to pick Python to be a tool for the job.

I remember being in one of big government project pitch session and the project manager asked "What this Python language? Sounds like some toy."

Times will change. However Rust sweet spot is system programming. Even though one could build e.g. websites with it, Rust loses its edge and becomes verbose, a too heavy hammer.


We (commure.com) are in the health care industry using Rust exclusively in the backend operating in SF, Boston and Montreal.

OneSignal (SFBA) and Sentry (SF, Austria) use Rust. I was actually surprised half a year ago when I started looking for companies using Rust that there were many more than I expected.


We at https://prisma.io are 50% Rust and 50% TypeScript since the beginning of the year. At least what I know we do no crypto ;)


We (as an organization) are a couple years away from doing Rust for embedded work, but I'll be pushing hard in that direction after I'm more comfortable with it (I'm the tech lead in the organization), in part because I dislike C++ so, so much.


Do you think there is a place for D in embedded? Maybe if you could use the stdlib without a GC?


Maybe. I've looked at D here and there, and while nice enough, that wasn't enough for me to really go into it.

Rust, on the other hand, has a lot of momentum behind it in the embedded space. Bare metal and embedded RTOS options.


I would consider digging into WASM. I think there will be a lot of demand eventually and Rust has a good story there.

Rust is slowly gaining momentum. Jobs are starting to pop up here and there.


I am a full time Go developer learning rust on the side for more low level things, and one of my side projects is writing an OS.

Rust's error handling really feels the exact same way to Go's in my opinion, the only difference is, so far in my admittedly newcomer viewpoint, using unwrap to get to the error value, or returning an error value and doing an if err != nil check.

This isn't to say anything other than agreement that I indeed like this style. I like to handle my errors as values and program around them.

The thing I don't get is why all the bikeshedding and hate around "if err != nil" spam I seem to have seen here often and on other blogs. Go seems to get this "criticism" (I don't really believe it is a valid critique, for what it is worth) heaped onto it plentifully, while Rust seems generally immune to it, otherwise praised for the approach -- but it is the same approach! In fact you even have a builtin panic func in go!

Can someone tell me what I may be missing?


A lot of the Rust developers I've spoken with don't really consider using unwrap everywhere the way that Go programmers use "if err != nil" to be proper error handling. Most of them consider it an ugly hack to be used when playing around or testing something.

The value that most people see in Rust's error types is in error transformation and propagation, which let you do things like correctly handling edge cases when using closures without cluttering up the logic of the success case.

If you are going to compare unwrap/expect to "if err != nil" -- Rust's advantage is that it forces you to at least pretend to handle the error, while Go will happily keep chugging along even if you never inspect err.


> Rust's advantage is that it forces you to at least pretend to handle the error

I'm not sure I would call this an advantage. See: Handling of Checked Exceptions in Java.


Checked expressions suffered from a woefully insufficient language to describe what might be thrown. For instance, there was no good way to type a map function.


Sort of seems even worse in that there is no built in way to propagate the error if you don't want to handle it. At least with Java you just added the exception to the throws declaration of your function if you didn't want to handle it. Here you have to do something about it or manually do something to propagate it? I think the failure of checked exceptions in Java to be a really interesting example to study for language designers, because on the surface, it seems like it was mostly a good idea. Yet in practice it was nearly universally disliked.


When in Java you add a method call with an exception you either handle it or add the method call and the exception type to the list. In Rust when you add a method call that has a Result, you either handle it or add ? to the end of it to propagate the error, auto converting to the appropriate error type. The experience is much better than on Java, while still properly documenting where failures can happen and what isn't being handled.


> you either handle it or add ? to the end of it to propagate the error, auto converting to the appropriate error type

But that can only happen if the method returns a type of Result, true? Does this have the effect that just about every function ends up having to return Result (so that the ? operator works)?


Pretty much, which means that the existence of fallible functions is exposed or handled. The big difference with explicit exceptions in Java is that Rust has enums that let you encode every error (so the type checker shows you when you add a new error type where you haven't handled it) so the list of possible errors goes in your enum, not the function definition or alternatively lets you rely on trait objects, which you can't match on but you don't care about when moving them around, which can be equivalent to relying on the root of the an object inheritance tree.


> Sort of seems even worse in that there is no built in way to propagate the error if you don't want to handle it.

That's better, not worse, because it means you don't up with unhandled downstream errors bloating the signature just because people didn't want to bother with them, which means people are more likely to do something sensible (which may just be wrapping) with them.


While its true that one can use `unwrap`, pattern matching and other higher order methods available for the Result type are a more idiomatic way to work with these.

For example:

    match <something that returns result> {
        Ok (v) => <do something with v>,
        Err (some error) => <handle error>
    }
By using the result type, the compiler ensures that the only way to get a value out of the result type is via pattern matching. So the errors are still values, except there is no need to come up with a default value for the error scenario, and there is no need to come up with an "empty" value.

In isolation it might look verbose, but rust also provides a lot of higher order operations that one can do on result types. Ex:

    myResult.map(<do-something>).and_then(sq).or_else(<do-something-else>);
Something else that I personally like about a result type is that it also serves as documentation by clearly marking operations that can fail, and have a consistent way for the end-user to know which value indicates that the response is a success or an error.

EDIT: Formatting


In Go, you don't have to check err != nil to get the underlying value, in Rust you do. You probably aren't missing that exactly, in that you probably knew that difference, but you may be missing its importance. It means that in Rust, it is diminishingly rare to have a value of some type which it is not valid to use, whereas in Go many or most values - any value returned from a function that also returns an error, which is a lot of them - may be invalid (because the error may not have been checked). In mature teams using Go it may be unlikely for any errors to be unchecked, but it is a gun that is always pointed at your foot.


Since Rust supports generics and algebraic data types, you can clearly denote on your return type what kind of errors can be expected and include useful data pertaining to each one, with the compiler making sure everything in its place; as opposed to casting from the error interface in a brittle manner prone to breaking on changes with no help from the compiler.

Since Result is also a functor in practice, you can chain modifications to the happy path without having to check errors when you don't need to and without having to write as much boilerplate.


You can do `.unwrap()` or `.expect(..)` to get your values, and in that respect, it's not that different from working in a language with `null`.

But you can actually almost totally avoid calling `unwrap`/`expect` in Rust code by doing pattern matching.

` match x { Some(value) => <DO SOMETHING WITH VALUE> None => <HANDLE THIS ERROR PATH>, }`.

If you always do that, then you've got the compiler reminding you to handle the case where it is `null`. That means that you never hit the equivalent of a null exception.

That being said, I still find myself doing some `unwrap` and `expect` (particularly in tests). But relying on matching most of the time as a first line of defense will prevent the equivalent of a null exception in the bulk of your code.


The best parts of rust's error handling are the try!() macro (aka `?`), as well as From/Into error types allow the compiler to wrap types or convert between error types. This eliminates almost all of the go boilerplate `if err != nil` and you only handle the error where you actually need to with pattern matching and destructuring, instead. It is much nicer. Go's error handling is pretty primitive, in my opinion.


>The best parts of rust's error handling are the try!() macro (aka `?`),

Go's way of providing try!() macro is less magical but almost as useful.[1]

> From/Into error types allow the compiler to wrap types or convert between error types

error is an interface in Go which can be easily cast/checked for the underlying type.

> Go's error handling is pretty primitive

I wouldn't call it primitive, I would call it simple. I like the comparison i read on a blog on HN.

Rust is the 'new' C++ and Go is the new C

[1] https://blog.golang.org/errors-are-values


> error is an interface in Go which can be easily cast/checked for the underlying type.

Yeah it's done during runtime and the compiler won't be able to help you with it if you fail to do exhaustive type checking. It's a problem anytime you refactor your code. ADTs and pattern matching is pretty much the bare minimum language feature i expect from any statically typed language.


It seems like Go's error handling is basically like node's err callback argument, maybe a little cleaner. But it's still not that strict in forcing you to deal with errors. You could just rename err to _ and just drive on.

In Rust, you either have to deal with the error, or explicitly ignore them. And if the error is the type that warrants crashing, that's easy too.


If you'd like to see something that's not "if err != nil", I recommend looking into elixir; there is the option of doing ok/error tuples, and there is a more ergonomic railway programming structure called "with". However, as it is a BEAM language, often the best solution is to "let it fail" as an error handling strategy. Let the process raise and crash, and a supervisor will restart it in a safe state if necessary.


I don't particularly like the solution presented.

1. The Rust community is biased against the usage of "unwrap". It was supposed to make prototyping easier. It should not have been there and should not be used.

The correct way will be to "evaluate" the function and either return a result or "propagate" the error to some part of your application that will handle "all" the errors. Whether they are your code errors, or errors thrown by other libraries.

2. The code will look even more succinct with the "?" operator:

> let value = some_operation_that_might_fail()?;

3. If the function fail, the program should handle the failure. By design, a failure should not result on setting a "default value". That's not an error but more like an "option".

The correct type the guy should have used is the Option type. Or more like an option inside a result. The option could return a value or not. If not, you set a default value.


> 1. The Rust community is biased against the usage of "unwrap". It was supposed to make prototyping easier. It should not have been there and should not be used.

I'm a little confused on this point you are making. You say the Rust community is biased against `unwrap` which makes it sound like you think that is wrong but then you proceed to say it shouldn't be there, making it sound like you agree.

`unwrap` has legitimate uses in product code as an assertion. Normally I recommend `expect` instead so that the assertion is self-documenting but there are times where the the scope of the assumption you are making is so small, it is obvious and doesn't need a comment, for example if I partition a collection based on the result [0]

[0]: https://doc.rust-lang.org/stable/rust-by-example/error/iter_...

> . If the function fail, the program should handle the failure. By design, a failure should not result on setting a "default value". That's not an error but more like an "option".

From the function's perspective, it errored, but from your perspective, it failed and you want to fallback to a default value.

Yes, someone could express this instead as an `Option` or a `Result<Option, _>`

- Sometimes you just don't need that extra boiler plate - Sometimes you want to provide context for why something failed in case the user considers it an error as well. You can't express that with `Option`.


> I'm a little confused on this point you are making. You say the Rust community is biased against `unwrap` which makes it sound like you think that is wrong but then you proceed to say it shouldn't be there, making it sound like you agree.

Nope. I think unwrap shouldn't be used without understanding the error handling capabilities of Rust. It also shouldn't be used for deployed software. The problem is that most people introduced to Rust would like to see something working now and Rust has higher barrier to entry than most mainstream languages. They'll likely keep the habit.

> From the function's perspective, it errored, but from your perspective, it failed and you want to fallback to a default value.

That's not my point. My point is that a failed function should not return a "default value". A default value is not a failure but rather a design decision. Should the function fail, it returns an error.


> It also shouldn't be used for deployed software.

This is equivalent to saying that `&vector[i]` shouldn't be used in deployed software, since it's just syntactic sugar for `vector.get(i).unwrap()`. It's also equivalent to saying that `assert!(...)` should never be used in deployed software. Or `unreachable!()`. Or any number of other things.

unwrap is perfectly fine for use in production. Stating a requirement in terms of unwrap doesn't make sense outside of niche scenarios. It's too specific and not really actionable in a lot of cases. A better way to put it is that programs that surface a panic to end users of the application have a bug. If the panic is from inside of a library and not the result of documented preconditions on a function, then the library has a bug. Otherwise, the bug is in the application.


While I too love Rust's error handling, this explanation of it seems incomplete without discussing the ? operator and its interaction with the From trait. The convenience methods on Result itself are nice of course but they aren't really the main mechanism of error handling in my opinion.


So what do you do when you want centralized error handling, which is the whole point of try-catch idiom?

>he second side effect is that it forces you to think about how to handle possible errors.

Java does this with throws E and (at least in Java) it sucks and I very much prefer C#'s implicit throws. In fact, I don't think it goes far enough. If you have complex error recovery, the language should allow you to completely separate good executions paths from the bad paths. Scope guards are a step in the right direction.

How would Rust code looks like if the error handling logic was something like "try X, if it fails try again, if that fails schedule another attempt via a queue, log a warning; if scheduling fails log a fatal error"?

Sure, handling this stuff with try-catch looks horrible. But I've thought long an hard about how to simplify it and using tricks like .unwrap_or (which you can simulate in C# to some extent) does not cut it, because error handling often requires to operate on method-scoped variables (e.g. for logging).


Depends what exactly you're looking to do. You can propagate errors with the `?` operator. And you can match on the specific error at a higher level.

The docs[0] do give a more complex example of error matching:

    fn main() {
        let f = File::open("hello.txt");

        let f = match f {
            Ok(file) => file,
            Err(error) => match error.kind() {
                ErrorKind::NotFound => match File::create("hello.txt") {
                    Ok(fc) => fc,
                    Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
                },
                other_error => panic!("There was a problem opening the file: {:?}", other_error),
            },
        };
    }
Although they also provide a slightly simpler alternative for this particular case.

[0] https://doc.rust-lang.org/stable/book/ch09-02-recoverable-er...


Doesn't look that much cleaner than try/catch, to be honest.

What I would really like to see in C#:

  public int DoThing(int y) 
  {
    //logic
  } 
  catch (SomeException e)
  {
    //handle things
    return 0;
  }
This would remove a lot of useless syntactic garbage without the need for any radical changes in the language. (It would translate to try-catch that wraps the entire body of the method.)

But so far I gave up on syntax-level error handling of any complexity. Generally, I just write multiple "agents" and all the error handling, retries and recovery is simply done by separate agent(s). The agent that tries to perform the initial actions simply records that it failed in some persistent storage and terminates or goes on to do something else.


> It would translate to try-catch that wraps the entire body of the method

It would not not be the same unless you change how scoping works in these instances, and that can introduce other complexities, e.g. how to limit scope so that some things are inaccessible to the catch block.


This:

  public void DoThing(){
     //code
  } catch (Exception x){
  } finally {
  }
would simply be syntactic sugar for this:

  public void DoThing(){
    try {
      //code
    } catch (Exception x){
    } finally {
    }
  }
It's consistent.


Yes, and that only works by changing how scoping works. Unless of course you don't care about the variables created inside.

i.e.

    public void DoThing()
    {
       var someVar = SomeStaticClass.FooFunc();
       someVar.DoSomething();
    } 
    catch (Exception x)
    {
       // Scoping rules will need to change
       // to put someVar in scope:
       _logger.Log($"{someVar.GetId()} is invalid");
    } 
    finally 
    {
       someVar.Cleanup();
    }
And this also implies that there's no way to make a variable declared inside the method inaccessible to the catch block without additional changes to the language.


>Unless of course you don't care about the variables created inside.

In majority of cases I really don't care about the variables created inside. If I can't handle an error just by looking at the exception and method arguments I usually refactor. In my view, error handling should not be intertwined with the "happy path". It makes reasoning about non-trivial code much harder.

For other cases the old syntax will still be available.

And yes, the value of "finally" in such a shorthand is dubious. I just added it to make it clear how the rewrite rule would work.


TensorFlow has a StatusOr C++ class that allows functions to return a value that can be first checked to be result.ok() (no error), and then the actual value can be unwrapped. It’s Google’s way of not using exceptions.

https://github.com/tensorflow/tensorflow/blob/38c762add3559b...


Designs like Java which have NullPointerException simply ignore null. Modeling T instead of T | null is just too convenient. It's a lazy design.

I'm fine with most of those new ways of error handling, although some feel better and the others feel a little bit awkward. If some concept exists (exception/emptiness/async), model it, handle it. Instead of just ignore it and leak the responsibility to the users.


Not being familiar with Rust, the article was interesting, but I felt like it lacked examples. The Result<T, E> is also available in other languages, even in TypeScript you could have `function x(): number | Error {}`, it is just a consequence of being able to return a union of data types, why is Rust's case special? (apart from being able to handle it nicer with "match")


Rust’s is a tagged union so you can have Result<number,number>, something you can’t have in Typescript.


You can have `Result<number, number>` in both.

You generally have to treat `E` as an error because of the semantics of `.map()` in every language, though, because you have to essentially ignore one of the sides. `map<Result<T, E>>` is untypable for any practical scenario where both sides matter equally.

Also, just to clarify how you can have essentially the same thing you would in f.e. Haskell but in TypeScript:

```

    export type Result<T, E> = Ok<T, E> | Err<T, E>;

    interface Ok<T, E> {
      type: "Ok";
      unwrap(): T;
      map<B>(f: (x: T) => B): Result<B, E>;
      toMaybe(): Just<T>;
      or(errorValue: T): T;
      apply<U>(f: Result<(x: T) => U, E>): Result<U, E>;
      bind<U>(f: (x: T) => Result<U, E>): Result<U, E>;
    }

    interface Err<T, E> {
      type: "Err";
      unwrap(): E;
      map<B>(f: (x: T) => B): Result<B, E>;
      toMaybe(): Nothing<T>;
      or(errorValue: T): T;
      apply<U>(f: Result<(x: T) => U, E>): Result<U, E>;
      bind<U>(f: (x: T) => Result<U, E>): Result<U, E>;
    }
```


Right, but in Typescript you can't match on the type at compile time, you have to add a "type" field with a string tag and match on it at runtime.


Well, the compiler constrains the type automatically as far as it can go, so a `switch` on the `.type` property will constrain it down to the specific case in the union. Practically speaking it's not all that different to f.e. Haskell, except you're manually tagging the different cases. You get exhaustiveness checks when checking cases by adding a `assertExhaustive(value: never)` in your `default:` case, which will only match if you handled all the cases.

For development purposes this is all essentially the same as proper sum types, _with worse ergonomics_.

My point was mostly that practically speaking you can have a full featured `Result<T, E>` type that is certainly good enough. What TypeScript lacks is what most halfway languages lack; higher-kinded types, etc., and good ergonomics that follow, such as type classes. These are also not a given in more esteemed type system circles: OCaml doesn't have them either (and most features that are "coming soon" in OCaml are bordering on vaporware).

Most nice type system features you can get by way of combinations of 2-3 distinct type system features in TypeScript. In terms of end results the only things that differentiate TypeScript from languages with more traditional type systems is that you have to use `TSLint` to absolutely eradicate `any` from usage completely (wheras you don't have it at all in a better type system) and the ergonomics for functional programming just aren't that great in JS.

This isn't the TS part of the syntax that's the issue, it's just that anyone who's used a ML descendant in the last 40+ years will have noticed that (automatic) currying is a massive boon to functional programming (and on top of that C-syntax is just about the worst for FP with all the noise that comes with just calling functions). Even something small as not being able to define operators is pretty disruptive to nice code. People like to rag on languages that let you do this, but there are many patterns in FP that absolutely are best encoded via well known and generally accepted operators.


Really nice! The Result reminds me of Validation from Scala.

In Scala you return a Validation that is either a value or an error, and can pattern match on it. Also: trySomething().orElse("Default value")


To be fair. The idiomatic C# would be:

    if(TrySomeOperationThatMightFail(out var value))
    {
    }

But even with that I often wish it was more like the Rust example


Except 99% of methods are async in a web app where the out variable does not work.


This is an extremely common pattern in Erlang and is super useful so it's nice to see Rust embrace it.


The article is a good opinion, but lacks necessary wider discussion in the context of all programming.

1) Is Rust error handling somehow better than in a programming language X?

2) Is it because of Rust or because standard libraries or something else (developer laziness, writing silent exception eating)

3) Why other programming languages choose to do it other ways?

Now the article reads like Rust education, which is good, but other commenters here find it very subjective.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: