Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

For a lot of stuff what I really want is golang but with better generics and result/error/enum handling like rust.


Me too. There’s a huge market for a natively compiled language with GC that has a better type system than Go.

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.


C#?


Also Haskell, Java, Kotlin, Scala, OCaml, D, and the list goes on.


Out of all those only Java and Kotlin captured significant market like OP mentioned


Those two aren’t natively compiled. They can be, but it’s not the norm, and it’s hard/time consuming.

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.


> Those two aren’t natively compiled

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.


Trivial apps are easy. Non trivial apps, lots of reflection, etc make it really hard.


Compared to what? Because compared to go which has not one but 2 nulls, and an even more anemic type system it is surely much better.

> 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.


I started this thread criticizing Go. I think it’s a terribly verbose language that has ignored all language dev of the last half century. It’s why I continue to write Java.

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.


Well, it's far from trivial to introduce such a change if you don't want to throw away 3 decades of code/assumptions. Of course null safety would be very welcome, I'm with you on this.

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).


Nah man. They could make checked exceptions less boiler platey. Simple things like Swift’s try! Or try?, and making the try construct an expression would go a long way to increase checked error usage and therefore correctness. We don’t even need to solve the lambda problem. We just need to make it easy to deal with checked exceptions. Right now you have a minimum 5 lines of boiler plate to escape them.


Making it an expression would indeed by nice - I do like that in Kotlin, though it's still not particularly short there (but given that there are not really checked throws there, it's easy to just have a lambda for that).

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.


Yes there was a proposal by Brian to make it into a switch expression: https://news.ycombinator.com/item?id=40088719, though that draft is already 1.5 years old and I don't expect it to be delivered anytime soon. It would alleviate some pain, but I really think we need something like try! to escape the compiler when some errors aren't possible:

    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 funky

You 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.


Is not natively compiled.


C# can also be compiled AOT.


With the same difficulties as Java.


But it's improving fast. They even made the new file-based apps target Native AOT by default (https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotn...).


I really do wish them luck! C# is a really nice language and I can't wait to see what they do once they introduce sum types. I've been doing Advent of Code in F# and it's been pretty nice.


Have you tried OCaml? With the latest versions, it also has an insanely powerful concurrency model. As far as I understand (I haven't looked at the benchmarks myself), it's also performance-competitive with Go.


How's the build tooling these days? Last I tried, it used some jbuild/dune + makefiles thing that was really painful to get up and running. Also there were multiple standard libraries and (IIRC) async runtimes that wouldn't play nicely together. The syntax and custom operators was also a thing that I could not stop stubbing my toes on--while I previously thought syntax was a relatively unimportant concern, my experience with OCaml changed my mind. :)

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?


I'm re-discovering OCaml these days after an OCaml burnout quite a few years ago, courtesy of my then employer, so I'm afraid I can't answer these questions reliably :/

So far, I like what I've seen.


    $ dune init project my-project
    $ dune build
That's it, now you have a compiling project and can start hacking.


Ocaml community is chill and helpful, and dune works great with really good compilation speeds.

Its a really nice language


Yea, there's not much for large scale production ocaml though, do it would be a tough sell at my work. It's one of those things where like.... if I got an offer to work at jane street I might take it solely for the purpose of ocaml lol.


I agree that if you’re learning a language primarily to find a job, OCaml isn’t currently the strongest choice, aside from the positions that companies like Tarides offer. However, if your goal is to learn a language that will genuinely improve your programming skills, I’d say OCaml is one of the best options.


There's also OCaml at GitLab and Semgrep, if you're on the market :)


Fair lol

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


There's also ReasonML if you want an OCaml with curly braces like C. But both are notably missing the high-performance concurrent GC that ships with Golang out of the box.


As far as I understand, OCaml's recent multicore GC is pretty good.

I haven't looked at benchmarks, though, so take this with a pinch of salt.


I thought ocaml programs were a little confusing about how they are structured. Also the use of Let wasn't intuitive. go and rust are both still pretty much c style


I think you’d need only a few hours to get used to the let style. Other syntaxes may feel more intuitive simply because we’ve seen more C-style code, but there’s nothing intrinsically “superior” about them. For me, the match syntax, the function signatures, the |> operator, and OCaml’s approach to shadowing are all very readable... often easier to grasp than the equivalent constructs in many other languages.


You want https://github.com/borgo-lang/borgo, but that project is dead. You might be interested in Gleam?


I cautiously agree, with the caveat that while I thought I would really like Rust's error handling, it has been painful in practice. I'm sure I'm holding it wrong, but so far I have tried:

* 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.


FWIW `fmt.Errorf("opening file %s: %w", filePath, err)` is pretty much equivalent to calling `err.with_context(|| format!("opening file {}", path))?` with anyhow.

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 :)


> 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.

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.


Yeah, there's a mismatch in vocabulary between Go and Rust developers.

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, nobody seems to care about that, which of course makes code easier to write, but catching errors quickly becomes stringly typed.

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.


> In Go we just use errors.Is() or errors.As() to check for specific error values or types (respectively). It’s not stringly typed.

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.


> 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?

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.


I will at least remark that adding a new error to an enum is not a breaking change if they are marked #[non_exhaustive]. The compiler then guarantees that all match statements on the enum contain a generic case.

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.


Indeed, there's almost always a solution to "inergonomics" in Rust, but most are there to provide a guarantee or express an assumption to increase the chance that your code will do what's intended. While that safety can feel a bit exaggerated even for some large systems projects, for a lot of things Rust is just not the right tool if you don't need the guarantees.

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.


If you're willing to do what you're saying in Go, exposing the errors from anyhow would basically be the same thing. The only difference is that Rust also gives all those other options you mention. The point about other people saying not to do it doesn't really seem like it's something you need to be super concerned with; for all we know, people might tell you the same thing about Go if it had the ability for similar APIs, but it doesn't


> I also don't love enums for errors because it means adding any new error type will be a breaking change

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.


You have to chill with rust. Just anyhow macro wrap your errors and just log them out. If you have a specific use case that relies on using that specific error just use that at the parent stack.


I personally like the flexibility it provides. You can go from very granular with an error type per function and an enum variant per error case, or very coarse with an error type for a whole module that holds a string. Use thiserror to make error types in libraries, and anyhow in programs to handle them.


I thought the recent error proposal was quite interesting even if it didn't go through: https://github.com/golang/go/issues/71528

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 was a big fan of the original check handle proposal: https://go.googlesource.com/proposal/+/master/design/go2draf...

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.


Didn't they say they're not accepting any new proposals for error handling?

I kinda got used to it eventually, but I'll never ever consider not having enums a good thing.


OCaml is the closest match I'm aware of.


Borgo [1] is basically that.

Though I think it's more of a hobby language. The last commit was > 1 year ago.

[1] https://news.ycombinator.com/item?id=40211891


Closest is probably C# but its still primarily an OOP driven language


I would say, C# supports more functional programming than Go. Go is more imperative than a functional language.


I think generics ruined the language. Zig doesn’t have them


But it has something for it (compile time evaluation of functions).


Are you familiar with Zig's error handling? It's arguably more Go-like than the Rust approach.


No, Zig's error handling is decent - you either return an error or a value and you have some syntactic sugar to handle it. It's pretty cool, especially given the language's low-level domain.

Meanwhile Go's is just multiple value-returns with no checks whatsoever and you can return both a valid value and an error.


But sometimes it is useful to return both a value and a non-nil error. There might be partial results that you can still do things with despite hitting an error. Or the result value might be information that is useful with or without an error (like how Go's ubiquitous io.Writer interface returns the number of bytes written along with any error encountered).

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.


Then just return a value representing what you want, instead of breaking a convention and hacking something and hoping that at use site someone else has read the comment.

Also, just let the use site pass in (out variable, pointer, mutable object, whatever your language has) something to store partial results.


> instead of breaking a convention and hacking something and hoping

It's not a convention in Go, so it's not breaking any expectations


But in most cases you probably want something disjoint like Rust's `Result<T,E>`. In case of "it might be success with partial failure", you could go with unnamed tuples `(Option<T>,E)` or another approach.




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

Search: