The options I’ve seen so far are: OCaml, D, Swift, Nim, Crystal, but none of them have seen to be able to capture a significant market.
Java’s type system isn’t as strong as it could be either. It is still lacking proper compile time support for null and there’s been no investment in making error handling better. I’ve written it every day for 10 years and the type system definitely doesn’t help you write correct programs.
> the type system definitely doesn’t help you write correct programs.
It surely helps significantly. You are just looking for even more from the type system, but that's another (fair) statement to make.
However, I don’t think that shields Java from its inability to make the language better. We still don’t have checked nulls and at this rate, even though there’s a draft JEP, I am not sure we will get them within this decade. The community still blindly throws unchecked exceptions because checked exceptions have received no investment to make them easy to work with.
The point of this thread is that people do want that. They want a natively compiled language (by default), that has checked nulls, errors represented in the type system, and has a GC.
As for unchecked exceptions, that may be a bit of an "unreasonable ask". The only language that properly solves the problem are languages with effect types, which are an active research area. Every other language have either FP-like error values, or just unchecked exceptions (and there are terrible "solutions" like errno and whatever go does), or most likely both. E.g. Haskell will also throw exceptions, not everything is encoded as a value.
In my opinion both is the most reasonable approach, when you expect an error case, encode it as the return type (e.g. parsing an Integer is expected to fail). But system failures should not go there, exceptions solve it better (stuff like the million kind of connection/file system issues).
I believe there was a proposal to incorporate it into the switch expression? That may make it slightly too complex though, with null handling and pattern matching.
A a;
try {
a = someThrowingFn();
} catch (AException ex) {
throw new IllegalStateException(ex); // not possible
}
becomes var a = try! someThrowingFn();
or with Brian's proposal: var a = switch (someThrowingFn()) {
case A anA -> anA;
case throws AException ex -> throw new IllegalStateException(ex);
}
...still a bit verbose and funkyYou should check out Kotlin's proposal for error unions, I think it's pretty good and prevents a lot of boiler plate associated with results/exceptions: https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0441.... They propose a similar construct to try! with !! like they have for nullable types.
Idk how up to date ou are in .NET but so you can have an idea how trivial it is in C#:
echo ‘Console.WriteLine(“hello world”);’ >> app.cs
dotnet publish app.cs
That’s it. By default C# is natively compiling.Also, at least at the time, the community was really hostile, but that was true of C++, Ada, and Java communities as well well. But I think those guys have chilled out, so maybe OCaml has too?
So far, I like what I've seen.
Its a really nice language
$ dune init project my-project
$ dune build
That's it, now you have a compiling project and can start hacking.I haven't looked at benchmarks, though, so take this with a pinch of salt.
Though as a side note I see no open gitlab positions mentioning ocaml. Lot of golang and ruby. Whereas jane street kinda always has open ocaml positions advertised. They even hire PL people for ocaml
* thiserror: I spend ridiculous and unpredictable amounts of time debugging macro expansions
* manually implementing `Error`, `From`, etc traits: I spend ridiculous though predictable amounts of time implementing traits (maybe LLMs fix this?)
* anyhow: this gets things done, but I'm told not to expose these errors in my public API
Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?
And when I ask these questions to various Rust people, I often get conflicting answers and no one seems to be able to speak with the authority of canon on the subject. Maybe some of these questions have been answered in the Rust Book since I last read it?
By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.
You can annotate your error enum with #[non_exhaustive], then it will not be a breaking change if you add a new variant. Effectively, you enforce that anybody doing a match on the enum must implement the "default" case, i.e. that nothing matches.
What `thiserror` or manually implementing `Error` buys you is the ability to actually do something about higher-level errors. In Rust design, not doing so in a public facing API is indeed considered bad practice. In Go, nobody seems to care about that, which of course makes code easier to write, but catching errors quickly becomes stringly typed. Yes, it's possible to do it correctly in Go, but it's ridiculously complicated, and I don't think I've ever seen any third-party library do it correctly.
That being said, I agree that manually implementing `Error` in Rust is way too time-consuming. There's also the added complexity of having to use a third-party crate to do what feels like basic functionality of error-handling. I haven't encountered problems with `thiserror` yet.
> Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?
If you wish to make sure it's not a breaking change, mark your enum as `#[non_exhaustive]`. Not terribly elegant, but that's exactly what this is for.
Hope it helped a bit :)
Yea this is exactly what I'm talking about. It's doable in golang, but it's a little bit of an obfuscated pain, few people do it, and it's easy to mess up.
And yes on the flip side it's annoying to exhaustively check all types of errors, but a lot of the times that matters. Or at least you need an explicit categorization that translates errors from some dep into retryable vs not, SLO burning vs not, surfaced to the user vs not, etc. In golang the tendency is to just slap a "if err != nil { return nil, fmt.Errorf" forward in there. Maybe someone thinks to check for certain cases of upstream error, but it's reaaaallly easy to forget one or two.
In Go, `if err != nil { return nil, fmt.Errorf(...) }` is considered handling an error.
In Rust, the equivalent `.context(...)?` is considered passing an error. Handling it is about finding out what happened and doing something about it.
In Go we just use errors.Is() or errors.As() to check for specific error values or types (respectively). It’s not stringly typed.
> If you wish to make sure it's not a breaking change, mark your enum as `#[non_exhaustive]`. Not terribly elegant, but that's exactly what this is for.
That makes sense. I think the main grievance with Rust’s error handling is that, while I’m sure there is the possibility to use anyhow, thiserror, non_exhaustive, etc in various combinations to build an overall elegant error handling system, that system isn’t (last I checked) canon, and different people give different, sometimes contradictory advice.
errors.Is() works only if the error is a singleton.
errors.As() works only if the developer has defined their own error implementing both `Error() string` (which is part of the `error` interface) and either `Unwrap() error` or `Unwrap() error[]` (neither of which is part of the `error` interface). Implementing `Unwrap()` is annoying and not automatizable, to the point that I've never seen any third-party library doing it correctly.
So, in my experience, very quickly, to catch a specific error, you end up calling `Error()` and comparing strings. In fact, if my memory serves, that's exactly what `assert` does.
> I think the main grievance with Rust’s error handling is that, while I’m sure there is the possibility to use anyhow, thiserror, non_exhaustive, etc in various combinations to build an overall elegant error handling system, that system isn’t (last I checked) canon, and different people give different, sometimes contradictory advice.
Yeah, this is absolutely a problem in Rust. I _think_ it's moving slowly in the right direction, but I'm not holding my breath.
However, I wouldn't recommend it. Breakage over errors is not necessarily a bad thing. If you need to change the API for your errors, and downstreams are required to have generic cases, they will be forced to silently accept new error types without at least checking what those new error types are for. This is disadvantageous in a number of significant cases.
On that topic, I've looked some at building games in Rust but I'm thinking it mostly looks like you're creating problems for yourself? Using it for implementing performant backend algorithms and containerised logic could be nice though.
Is it a new error condition that downstream consumers want to know about so they can have different logic? Add the enum variant. The entire point of this pattern is to do what typed exceptions in Java were supposed to do, give consuming code the ability to reason about what errors to expect, and handle them appropriately if possible.
If your consumer can't be reasonably expected to recover? Use a generic failure variant, bonus points if you stuff the inner error in and implement std::Error so consumers can get the underlying error by calling .source() for debugging at least.
> By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.
Nothing stopping you from doing the same in Rust, just add a match arm with a wildcard pattern (_) to handle everything but your special cases.
In fact, if you suspect you are likely to add additional error variants, the `#[non_exhaustive]` attribute exists explicitly to handle this. It will force consumers to provide a match arm with a wildcard pattern to prevent additions to the enum from causing API incompatibility. This does come with some other limitations, so RTFM on those, but it does allow you to add new variants to an Error enum without requiring a major semver bump.
My hope is they will see these repeated pain points and find something that fits the error/result/enum issues people have. (Generics will be harder, I think)
I kinda got used to it eventually, but I'll never ever consider not having enums a good thing.
I see the desire to avoid mucking with control flow so much but something about check/handle just seemed so elegant to me in semi-complex error flows. I might be the only one who would have preferred that over accepting generics.
I can't remember at this point because there were so many similar proposals but I think there was a further iteration of check/handle that I liked better possibly but i'm obviously not invested anymore.
Though I think it's more of a hobby language. The last commit was > 1 year ago.
Meanwhile Go's is just multiple value-returns with no checks whatsoever and you can return both a valid value and an error.
I appreciate that Go tends to avoid making limiting assumptions about what I might want to do with it (such as assuming I don't want to return a value whenever I return a non-nil error). I like that Go has simple, flexible primitives that I can assemble how I want.
Also, just let the use site pass in (out variable, pointer, mutable object, whatever your language has) something to store partial results.
It's not a convention in Go, so it's not breaking any expectations