Once every return is a Result, every call a ?, and every error a yeet, what's the difference between your program and one with exceptions except the Result program being syntactically noisy and full of footguns?
Better for a language to be exceptional from the start. Most code can fail, so fallibility should be the default. The proper response to failure is usually propagating it up the stack, so that should be the default too.
What do you get? Exceptions.
It means you can use things like result.map_err(|e| …) to transform an error from one type to another. (Though if there’s a suitable From conversion, and you’re going to return it, you can just write ?.)
It means you can use option.ok_or(error) or option.ok_or_else(|| error) to convert a Some(T) into an Ok(T) and a None into an Err(E).
It means you can .collect() an iterator of Result<T, E> into a Vec<Result<T, E>>, or (one of the more spectacular examples) a Result<Vec<T>, E> which is either Ok(items) or Err(first_error).
It’s rather like I found expression-orientation, when I came to Rust from Python: at first I thought it a gimmick that didn’t actually change much, just let you omit the `return` keyword or so. But now, I’m always disappointed when I work in Python or JavaScript because statement-orientation is so limiting, and so much worse.¹ Similarly, from the outside you might not see the differences between exceptions and Rust-style Result-and-? handling, but I assure you, if you lean into it, it’s hard to go back.
—⁂—
¹ I still kinda like Python, but it really painted itself into a corner, and I’ve become convinced that it chose the wrong corner in various important ways, ways that made total sense at the time, but our understanding of programming and software engineering has improved and no new general-purpose language should make such choices any more. It’s local-maximum sort of stuff.
As usual, I find that opposition to exceptions is rooted in a misunderstanding of what exceptions really are
Perhaps this also might be clearer: the fuss is not about the errors themselves being values, but about the entire error handling system being just values.
With exceptions they are thrown and end up somewhere else in a catch block, so the path of the success value is now distinct from the path of the error value.
Because errors as values are explicit. You're not forced to use ? everywhere; you can still process errors however you like, or return them directly to the calling function so they deal with it. They're not separate control flow like exceptions, and they're not a mess like Go's.
Yeah, yeah, you can say it's explicit and you can handle it how you want and so on, but the overall effect is just a noisy spelling of exceptions with more runtime overhead and fewer features.
In closed-world, system-software, or low-level software you want to have your kind of knowledge about everything you call. Even more: can it block?
In open-world, business-software, or high-level software it is often impossible or impractical to know all the ways in which a function or method can fail. What you need then, is a broad classification of errors or exception in the following two dimensions: 1. transient or permanent, 2. domain or technical. Those four categories are most of the time enough to know whether to return a 4xx or 5xx error or to retry in a moment or to write something into a log where a human will find it. Here, unchecked exceptions are hugely beneficial. Coincidentally, that is the domain of most Java software.
Of course, these two groups of software systems are not distinct, there is a grey area in the middle.
That's why it's often considered one of the better approaches: it has both the "errors as values" benefits (encoding errors in the type system) and avoids many of its problems (verbosity when you just want to bubble up).
...Or solve the problem. A library function that can be a source of issues and can't fix these issues locally should simply not be returning something that is not a result in that paradigm.
> since something can go wrong in most code
That is not my experience. Separating e.g. business logic which can be described cleanly and e.g. API calls which don't is a clear improvement of a codebase.
> the whole codebase converges as time goes to infinity on having Result everywhere.
As I said previously, it is pretty easy to pipe a result value into a function that requires a non-result as input. This means your pure functions don't need to be colored.
People "solve" this problem by swallowing errors (if you're lucky, logging them) or by just panicking. It's the same problem that checked exceptions in Java have: the error type being part of the signature constrains implementation flexibility.
I'd be curious to see examples for where you think Rust is still to verbose.
Kotlin isn't relevant here because all exceptions are unchecked.
Function coloring isnt a problem, it is just approach
• they may propagate automatically from any point in code, potentially breaking atomicity invariants and preventing forward progress, and have to be caught to be transformed or wrapped – Result requires an explicit operator for propagation and enables restoring invariants and transforming the error before it is propagated.
• they are an implicit side-channel treated in the type system like an afterthought and at best opt-out (e.g. "noexcept") – Result is opt-in, visible in the return type, and a regular type like any other, so improvements to type system machinery apply to Result automatically.
• try…catch is a non-expression statement, which means errors often cannot be pinpointed to a particular sub-expression – Result is a value like any other, and can be manipulated by match expressions in the exact place you obtain it.
Sure, if you syntactically transform code in an exception-based language into Rust you won’t see a difference – but the point is to avoid structuring the code that way in the first place.
A failure can propagate in the same circumstances in a Rust program. First, Rust has panics, which are exceptions. Second, if any function you call returns Result, and if propagate any error to your caller with ?, you have the same from-anywhere control flow you're complaining about above.
Programmers who can't maintain invariants in exceptional code can't maintain them at all.
> try…catch is a non-expression statement,
That's a language design choice. Some languages, like Common Lisp, have a catch that's also an expression. So what?
> they are an implicit side-channel treated in the type system like an afterthought an
Non-specific criticism. If your complaint is that exceptions don't appear in function signatures, you can design a language in which they do. The mechanism is called "checked exceptions"
Amazing to me that the same people will laud Result because it lifts errors into signatures in Rust but hate checked exceptions because they lift errors into signatures in Java.
Besides, in the real world, junior Rust programmers (and some senior ones who should be ashamed of themselves) just .unwrap().unwrap().unwrap(). I can abort on error in C too, LOL.
What do you consider failure? A result or a panic? Those are worlds apart.
> First, Rust has panics, which are exceptions.
That's not how exceptions work in Java. Exceptions are meant to be caught (albeit rarely). Panics are meant to crash your program. They represent a violation of invariants that uphold the Safety checks.
Only in extreme cases (iirc Linux kernel maintainers) was there a push to be able to either "catch panics" or offer a fallible version of many Rust operations.
The Rust version of Java Exceptions are Results. And they are "checked" by default. That said, Exceptions in Java are huge, require gathering info and slow as molases compared to returning a value (granted you can do some smelly things like raising static exceptions)[1].
[1] https://shipilev.net/blog/2014/exceptional-performance/
> Non-specific criticism. If your complaint is that exceptions don't appear in function signatures, you can design a language in which they do. The mechanism is called "checked exceptions"
And everyone hates checked exceptions. Because rather than having a nice pipe that encapsulates errors like Result, you list every minute Exception possible, which, if we followed the mantra of "every exception is a checked exception" would make, for example, Java signatures into an epic rivaling Iliad.
> Besides, in the real world, junior Rust programmers (and some senior ones who should be ashamed of themselves) just .unwrap().unwrap().unwrap().
If this is a game who can write worse code, you'll never find Java wanting. As a senior Java dev, I've seen things... Drinking helps, but the horrors remain.
Panics are fatal errors that the program is not expected to recover from, and it might as well terminate on the spot – as if you cut off power, which you have to be ready for no matter what. In fact, abort-on-sight-without-unwinding is one of the ways they can be configured to work. It’s the case where an operation fails, but the program attempts to continue running that is the most fragile. This is where you’re likely to enter an invalid state.
> you have the same from-anywhere control flow you're complaining about above
Not from literally anywhere – from the places where the error-propagation operator is used. I will never have myself thinking “oh wait, F could have thrown before G is invoked and break my invariants!” months after writing that code. C++ is chock-full of such problems: https://www.youtube.com/watch?v=b9ZYM0d6htg
> Some languages, like Common Lisp, have a catch that's also an expression. So what?
So more power to them! That is actually a major improvement compared to your typical dynamic ALGOL, where you find yourself stuck in situations like:
def foo():
try:
return d1[k1] + d2[k2]
except KeyError as e:
""" now what? which lookup failed? """
and have to resort to clumsy workarounds.> Amazing to me that the same people will laud Result because it lifts errors into signatures in Rust but hate checked exceptions because they lift errors into signatures in Java.
I don’t think you can write a generic over a checked exception list in Java, which means higher-order functions cannot deal with them. Modern implementations just don’t bother. Meanwhile, when errors are values, improvements to the type system in the happy path will apply to the error path as well, because they don’t use distinct language facilities that have to be separately considered and specified.
Your explanation of what bothers you with results seems to be focused on one specific way of handling the result, and not very clear on what the issue is exactly.
> what's the difference between your program and one with exceptions
Sometimes, in a language where performance matters, you want an error to be handled as an exception, there's nothing wrong with having that option.
In other languages (e.g. Elm), using the same Result pattern would not give you that option, and force you to resolve the failure without ending the program, because the language's design goals are different (i.e. avoiding in-browser app crash is more important than performance).
> syntactically noisy
Yeah setting up semantics to make users aware of the potential failure and giving them options to solve them requires some syntax.
In the context of a discussion about golang, which also requires a specific pattern of code to explicitly handle failures, I'm not sure what's your point here.
> full of footguns
I fail to see where there's a footgun here? Result forces you to acknowledge errors, which Go doesn't. That's the opposite of a footgun.
The Try operator (`?`) is just a syntax sugar for return. You are free to ignore it. Just write the nested return. People like it for succinctness.
Yeet? I don't understand, do you mean the unstable operator? Rust doesn't have errors either.
> what's the difference between your program and one with exceptions except the Result program being syntactically noisy and full of footguns?
Exceptions keep the stacktrace, and have to be caught. They behave similar to panics. If panics were heavy and could be caught.
Rust errors aren't caught, they must be dealt with in whatever method invokes them. Try operator by being noisy, tells you - "Hey, you're potentially returning here". That's a feature. Having many return in method can both be a smell, or it could be fine. I can find what lines potentially return (by searching for `?`).
An exception can be mostly ignored, until it bubbles up god knows where. THAT IS A HUGE FOOTGUN. In Java/C# every line in your program becomes a quiet return. You can't find what line returns because EVERY LINE CAN.
How I hate it, that every second function call can break my program when it's clearly not a "halt the world, it's totally unrecoverable that the user sent us nonsense" type.
Return a Result and get on with your life!
https://github.com/rust-lang/rust-clippy/issues/8184#issuecomment-1003651774
error: indexing may panic
--> src/main.rs:100:57
|
100 | rtmp::header::BasicHeader::ID0 => u32::from(buffer[1]) + 64,
| ^^^^^^^^^
|
= help: consider using `.get(n)` or `.get_mut(n)` instead
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#indexing_slicingI love Rust the language, but the current bazar of little bits of functionality scattered around in the form of a zoo of crates is such a mess compared to, say, Java's class library (I/O, data structures, std. algorthms for searching, sorting etc., arranged in a logically and hierarchially named way).
I'm not against alternative implementations, but I'd rather have one "official" implementation that everyone knows that covers most cases and makes for the idiomatic reading of source code.
I mean, yes, but not an official one.
The stdlib is where good ideas go to die.
Waiting for "pub struct RDBMSInferfaceV17ThisTimeWeGotItRightForSure"
Sylvain Kerkour described the problems Rust faces by having a limited standard library:
"The time has come for Rust to graduate from a shadow employment program in Big Tech companies to a programming language empowering the masses of engineers (and not just "programmers") wanting to build efficient and robust sfotware.
What crypto library should we use? ring, RustCrypto, rust-crypto (don't use it!), boring, aws-lc-s or openssl?
Which HTTP framework? actix-web, axum, dropshot or hyper?
What about a time library? time, chrono or jiff (how I'm even supposed to find this one)?
You get it, if you are not constantly following the latest news, your code is already technical debt before it's even written. Fragmentation is exhausting.
I just looked at the dependencies of a medium-sized project I'm working on, and we have 5+ (!) different crpyto libraries: 2 different versions of ring, aws-lc-rs, boring, and various libraries from RustCrypto. All of this because our various dependencies have picked a different one for their own cryptographic usage. This is insane, first because it introduces a lot of supply chain attack entry points, but also because there is no way that we will audit all of them"
Source: https://kerkour.com/rust-stdx
My moderate-sized Rust web service project requires 587 third-party crates which seems ridiculous.
Fortunately, cargo makes it easy to manage the dependencies, but unfortunately I don't know how well supported or maintained the dependencies are. How well will they be maintained in five years from now? Will I need to find newer libraries and rewrite portions of my project to provide the same features that I have now? I don't know.
I’ve seen this argument, but if you look at real golang code and examples, it’s just a bunch of “if err <> nill” copy pasta on every line. It’s true that handling errors is painstaking, but nothing about golang makes that problem easier. It ends up being a manual, poor-man’s stack-trace with no real advantage over an automatically generated one like in Python.
Also, exception systems usually come with built in stack trace support, "this error caused by this other error" support, debugger integration ("break the first time something goes wrong"), and tons of other useful features.
(Common Lisp conditions are even better, but you can't have everything.)
You can't just wave the word "goto" around as if it were self-evident that nonlocal flow control is bad. It isn't.
> And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
That's not been my experience at all. Writing exception safe code is a matter of using your language's TWR/disposable/RAII/etc. facility. A programmer who can't get this right is going to bungle explicit error handling too.
Oh, and sum types? Have you read any real world Rust code? Junior developers just add unwrap() until things compile. The result is not only syntactic clutter, but also a program that just panics, which is throwing an exception, the first time something goes wrong.
Many junior developers struggle with error handling in general. They'll ignore error codes. They'll unwrap sum types. They might... well, they'll propagate exceptions non-fat ally, because that's the syntactic default, and that's usually the right thing. We have to design languages with misuse in mind.
If you really don't like unwrap[1], you can enable a linter warning that will let you know about its uses to flag it during code review. You know exactly where they are and when they happen. Exceptions are hidden control flow, so you rely on documentation to know when a function throws.
> Writing exception safe code is a matter of using your language's TWR/disposable/RAII/etc. facility. A programmer who can't get this right is going to bungle explicit error handling too.
Rust has RAII, so you don't have to worry about clean-up when returning errors. This is a Go problem, not Rust.
At the same time Checked Exceptions were a nightmare as well, because suddenly they were part of the contract, even though maybe wrong later.
And what would you do in that case? Since this is a future change your existing code presumably wouldn't know what else to do but throw its own exception, so why not just let that one propagate?
But then over in Java world, your checked exception paradise (which it of course isn't because the syntax and toolkit for managing the things is so clunky) is easily broken by the number of unchecked exceptions which could be thrown from anything at any time and break your code in unexpected and exciting ways, so not only do you have to deal with that system you also don't get any assurance that it's even worth doing.
But this doesn't actually mean checked exceptions are a bad idea, it means that Java didn't implement them very well (largely because it also has unchecked exceptions, and NullPointerException is unchecked because otherwise the burden of handling it would be hideous, but that comes down to reference types being nullable by default, which is a whole other barrel of pain they didn't have to do, and oh look, Go did the same thing wooo).
Depends on the area you look at. Language documentation is pretty good and so is documentation for the standard library itself. Documentation for the frameworks can be hit or miss. EF Core is pretty well documented and it’s easy to find what to look for usually. GUI frameworks are more of a learning curve however.
FWIW many in Java community consider checked exceptions to be a mistake. While I don’t find writing code that has many failure modes particularly fun with exception handling - Rust perfected the solution to this (and the Go way is visually abrasive, no thanks), I don’t think it’s particularly egregious either - Try pattern is pretty popular and idiomatic to use or implement, and business code often uses its own Result abstractions - switch expressions are pretty good at handling these. Personally, I’d write such code in F# instead which is a recent discovery I can’t believe so few know how good it is.
1. Exceptions are expensive (at least in Java / C#), as they generate a stack trace every time. Which is fine for actually exceptional situations, the equivalent of `panic()` in Go, but:
2. Exceptions are thrown for situations that are not exceptional, e.g. files that don't exist, database rows that don't exist, etc. Those are simple business logic cases. The workaround is defensive coding, check if the file exists first, check if the row exists? that kind of thing.
3. The inconsistency between checked and unchecked exceptions.
4. Distance - but this is developer / implementation specific - between calling a function that can throw an error and handling it.
But #2 is the big one I think. Go's error handling is one solution, but if it's about correct code, then more functional languages that use the Either pattern or whatever it's called formally are even better. Go's approach is the more / most pragmatic of the options.
Ugh NO. Please don't. You should never "check if the file exists first". It can stop existing between your check and your later attempt at opening the file (the same with database rows). That can even lead to security issues. The name for that kind of programming mistake, as a vulnerability class, is TOCTOU (time-of-check to time-of-use).
The correct way is always to try to do the operation in a single step, and handle the "does not exist" error return, be it a traditional error return (negative result with errno as ENOENT), a sum type (either the result or an error), or an exception.
an exception is meant for EXCEPTIONAL behavior.
So it may be that the file access throws an exception but generally, I wouldn't agree.
Sure, you can only do it way up the stack, but that's not enough quite often.
If you can only do it all the way up, I find it ergonomic.
Maybe I should experiment more with catch unwind in Rust.
Junior developers will write suboptimal code in any language. So I'm not sure what your point is.
I shocked my Professor at university with that statement. After I started laughing, he asked me more questions... still went away with a straight A ;D
To me this proved that there's nothing inherently wrong about exceptions.
It's how you structure your code and the implied assumptions you share with your colleagues.
Some people are way too optimistic about their program will actually do.
Happy path programming.
Or how does one optionally throw and exception without an "if" statement? What's the "base" exception call?
Ie if "if" is implemented via exceptions, how do exceptions get triggered?
And is "while" done via an "if exception" and recursion? Or another way?
https://blog.plan99.net/what-s-wrong-with-exceptions-nothing...
> Even junior engineers have a trivial time debugging most go errors
Not my experience at all. I had to do this once. An HTTP request to a production server was yielding a 400 Bad Request with no useful information for what was bad about it. No problem, I'll check the logs and look at the source code. Useless: the server was written in Go and the logs had no information about where the error was originating. It was just getting propagated up via return codes and not logged properly. It ended up being faster to blackbox reverse engineer the server. In a language with exceptions there'd have been a stack trace that pinpointed exactly where the error originated, the story of how it was handled, and the story of how the program got there.
Diagnosing errors given stack traces is very easy. I've regularly diagnosed subtle bugs given just logs+stack trace and nothing else. I've also had to do the same for platforms that only have error codes that aren't Go (like Windows). It's much, much harder.
This is the most important aspect of exceptions in my view.
The line that threw the exception isn't even the part of a stack trace that I find most interesting. The part that is most valuable to me when working on complex production systems are all of the call sites leading up to that point.
I remember in my junior years I wasn't a big fan of exceptions. A stack trace would make my eyes glaze over. I would try/catch at really deep levels of abstraction and try to suppress errors too early. It took me a solid ~5 years before I was like "yes, exceptions are good and here's why". I think a lot of this boils down to experience and suffering the consequences of bad design enough times.
Still, better to have too much data than too little.
In your map example, just add a scope guard that removes the just-added element using the returned iterator if the rest of the procedure doesn't succeed. It's no different in Java.
That sort of thing can help yes. But it's still way harder to get exception safety right in a language with manual memory management. In a GCd language you only have to be careful about cleaning up non-GCd resources, whereas in C++ you have to be ready for the expected lifetimes of things to be violated by exception unwinds at many different points and if you get it wrong, you corrupt the heap. Very few C++ codebases use exceptions vs all of them for Java, and I think that's why.
Go has support for exceptions, not to mention providing runtime access to stack trace information in general. They are most definitely there if your application requirements necessitate, which it seems yours did. Unfortunately, language support only helps if your developers actually speak the language. Go in particular seems especially prone to attracting developers who are familiar with other languages, who insist on continuing to try to write code in those other languages without consideration for how this one might be different and then blame the technology when things don't work out...
Not true at all
* goto goes to a specific hard-coded address
* return looks up the previous address from the stack and goes there
* exceptions are a complex mess that require branching logic to determine where to resume execution
They are just horrific gotos that any library can invoke against your code. They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
Even junior engineers have a trivial time debugging most go errors, while even experienced principles struggle with figuring out the true cause of a Java exception.