Remix.run Logo
devjab a day ago

This article makes a lot of great points about the shortcomings of Go. I don’t think explicit error handling is one of them however. I’ve previously spoken about my loathing of exception handling because it adds a “magic” layer to things which is way too easy to mess up. From a technical standpoint that isn’t necessarily a good argument, but from a pragmatic standpoint and decades of experience… well I will take explicit error handling which happens exactly where the errors occur every day. You can argue that Rust does it in a more elegant way, and I prefer it for personal projects. For big projects with a lot of developers of various skill level joining and leaving I think Go’s philosophy is one of the sanest approaches to error handling in the modern world.

Staying in that lane. In my part of the world Go is seeing adoption that no other “new” language has exactly because of its simplicity. It’s not the best language, but it’s often the best general purpose language because it has a lot of build in opinions which protect you from yourself.

the_gipsy a day ago | parent | next [-]

There are several shortcomings with go's error handling. The author heavily lies onto rust, so the alternative is not exceptions but a `Result<T, Error>` sum type.

No stacktraces and error wrapping forces you to not only invent unique error messages. You must also conceive a unique wrapping message at every call-site so that you can grep the error message and approximate a stacktrace.

The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language, and the awkward variable initialization rules, make it so that you use the wrong `err` var at some point. E.g. if you want to reassign the result to an existing var, suddenly you have to declare `var err error`, and if `err` already exists then you have to reuse it.

There should be an enum type in go, or instead of the bizarre "return tuple" mechanics exclusive for errors, they should have added a better syntax sugar for errors like rust's `?` sugar. Instead we have something extremely tedious and quite error prone.

> it has a lot of build in opinions which protect you from yourself

It does have opinions, but too often they seem to be there to protect the language from being criticized. Sadly, this works, as marketing (lying) is an important factor towards making a PL popular in today's market.

masklinn a day ago | parent | next [-]

> The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language

MRV, go does not have tuples.

Go is not the only language with MRV (as a special case) and they’re not necessarily bad, iirc Common Lisp uses them as auxiliary data channels and has a whole host of functions to manipulate and refit them.

Go is singularly incompetent at MRVs though, in the sense that only syntax and builtin functions get access to variable arity (e.g. if you access a map entry you can either get one return which is a value, or two which are the value and whether the key is/was in the map). So MRVs mostly end up being worse tuples infecting everything (e.g. iterators needing Iter and Iter2 because you can’t just yield tuples to for loops).

robocat a day ago | parent | next [-]

> MRV

Acronym for Multiple Return Values https://gobyexample.com/multiple-return-values

pwdisswordfishz a day ago | parent | prev [-]

> MRV, go does not have tuples.

> MRVs mostly end up being worse tuples

I think you noticed yourself that you’re getting too hung up on terminology. Multiple return values are a half-hearted, non-reified version of tuples.

masklinn a day ago | parent | next [-]

No, MRVs can actually offer useful properties and features, that is what they do in Common Lisp. That Go does not do that has nothing to do with MRVs.

erik_seaberg 16 hours ago | parent [-]

f(g(), h()) can't work with MRVs, only f(g()). multiple-value-list, multiple-value-call, and quietly ignoring unneeded values makes them much more usable.

biorach a day ago | parent | prev [-]

Which is what they said. I'm not sure what point you're making

the_gipsy a day ago | parent | prev | next [-]

I forgot:

The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface.

IMO it's okay to make behaviour-exceptions specifically for error handling. Rust for example doesn't really have builtin behaviour exceptions specifically for errors, they're generic to sumtypes and just happen to work well for errors. But then in practice you must resort to thiserror or anyhow helper crates to deal with errors in anything but tiny programs.

If you do make behaviour exceptions for error handling, be honest and upfront about it. Don't say "errors are just values so just use them like regular vars" in docs, if then there are several huge exceptions (tuple-returns and breaking a core tenet). If you make exceptions then you might as well do them right, instead of half-assing them. I believe zig does it right, but I haven't gotten around to try it.

Seb-C a day ago | parent | next [-]

"Do what I say, not what I do" is almost a design guideline of Go at this point, there are so many inconsistencies like this in the language design.

9rx a day ago | parent | prev [-]

> The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface.

To be fair, that expression came from a blogger who often wrote about Go. It is not a tenet held by the Go project. In fact, Rob Pike has made clear that he considers it to be misguided advice. It is only violated in the same way returning Result in Rust violates the assertion I made up for this comment: Do not return Result.

the_gipsy a day ago | parent [-]

https://go.dev/wiki/CodeReviewComments#interfaces

wbl a day ago | parent [-]

How on earth is that violated by error?

error is implemented by types all over the standard library and beyond and consumed by functions that wrap errors in the errors package. It's exactly an example of what you claim is violated.

Even more obviously your link isn't talking about functions but packages. There are some violations out there but generally when included packages define interfaces they are ones that get consumed like in io or it's dissolution.

the_gipsy a day ago | parent [-]

> The implementing package should return concrete (usually pointer or struct) types

It's in the first paragraph. It goes on in the second:

> Do not define interfaces on the implementor side of an API “for mocking”; instead, design the API so that it can be tested using the public API of the real implementation.

And yes, io.Reader/Writer violate that too, because either the tenet is wrong or the design of interfaces in go is wrong.

> Even more obviously your link isn't talking about functions but packages.

It doesn't matter: your exporting errors behind the error interface, not your concrete error implementation. If you're just using one of the many ways to create stringly errors (!) like fmt.Errorf, you maybe don't notice but you are in fact returning interfaces all the time.

9rx a day ago | parent | next [-]

The actual first paragraph states:

> This page collects common comments made during reviews of Go code, so that a single detailed explanation can be referred to by shorthands. This is a laundry list of common style issues, not a comprehensive style guide.

The document does not assert that you must not return interfaces or that it is incorrect to return interfaces. It only indicates that returning interfaces at inappropriate times has been a recurring issue found during code review. Sometimes returning an interface truly is the right choice, but when it isn't...

Like most adages in programming, the aforementioned tenet holds validity in many cases, but, as always, "use your noggin" applies.

wbl a day ago | parent | prev [-]

It does matter that packages and functions are different.

It also matters what the io package actually does. https://pkg.go.dev/io . The io package has a very limited number of functions that return Readers. The vast majority of its functions take Readers or Writers as arguments and do useful things with them: e.g. Copy or LimitedReader. Most of the interfaces it defines (ReedSeeker, ReadWriteSeeker, etc) aren't instantiated by anything in it.

os implements Reader and Writer for filehandles. net does the same for sockets etc.

the_gipsy 21 hours ago | parent [-]

It doesn't matter with `error` because it's returned everywhere, both functions and from packages by proxy.

I'm not arguing that the tenet should be held true, to be clear. I'm saying that this tenet is misleading. If you can, return a concrete type. If several packages consume the same interface, then you it's not reasonable to define the interface at the consumer because you'd just have to copypaste it.

9rx 19 hours ago | parent [-]

> I'm saying that this tenet is misleading.

Doesn't that go without saying? There is no tenet that isn't misleading when presented to a general audience. Fair that if you come from a position where you understand the full context and nuance under which the tenet was built then you should be able to free yourself from being mislead, but, of course, this time is no exception.

> If several packages consume the same interface, then you it's not reasonable to define the interface at the consumer because you'd just have to copypaste it.

Where several packages find interface commonality, there is no doubt a set of "primary" functions that roll up shared functionality around that interface. The package of shared functions is understood to be the consumer under that scenario.

Where several packages stumbled upon the same interface without any shared functionality, copy/pasting is warranted. In this case, while the interfaces may look the same, they do not carry the same intent and that needs to be communicated. Another oft-misunderstood tenet, do not repeat yourself, does not imply avoid repetitive code.

the_gipsy 10 hours ago | parent [-]

I don't quite see when a package is "understood to be the consumer" of... itself? We're talking about other packages importing an interface.

I can give you a concrete example.

I have a "storage" package, that exports multiple storage implementations/backends. Files, in-memory, S3, zip...

Some other packages pick one of the implementations to instantiate, depending on the use case (this is NOT a case of mocking for testing or anything like that).

Most other packages work with a "storage" interface, as they're only concerned with reading/writing to "storage".

So the storage package, or in any case some package, has to export the interface. Otherwise, every consuming package would have to copypaste that interface, which is NOT warranted.

9rx 9 hours ago | parent [-]

Actual code is always better, but based on the description it seems a bit strange that there would be a single package that exports multiple implementations. Presumably each distinct implementation should be its own package. None of these packages would export the interface.

An additional package would export the interface and provide the common functionality around that interface. Those common functions would be considered the consumer. In fact, the standard library offers an example of exactly this: io/fs.

the_gipsy 5 hours ago | parent [-]

No, I cannot agree that this would be called the consumer.

Yes, you have technically moved the interface type away from the implementation, but just for the sake of it, without any other upsides. The consumer is still the package that is using and importing this interface type, just from another package now.

9rx 2 hours ago | parent [-]

That is the beauty of engineering: There is no universal truth, just different tradeoffs. Meaning that you don't need to agree, nor should you even seek agreement. You can and should forge your own path if different tradeoffs are warranted for your unique needs.

But, this is the "idiomatic" approach. The upside is consistency for future readers. Code is for humans to read, after all. Most codebases follow this pattern, so it will be familiar when the next person encounters it. If you have a reason to do things differently, go for it. Nobody knows your problem better than you, so you cannot listen to others anyway.

I am quite curious about what you see in the different tradeoffs you are accepting, though! What has you weighing them in favour?

the_gipsy 22 minutes ago | parent [-]

Sorry but I haven't really seen this pattern anywhere, care to give some examples?

All libraries that I recall ever using always export a single package, including interfaces. I just took a look, and even io exports a bunch of structs along the interfaces they implement. And `error` is like a basic type of the runtime.

9rx 2 minutes ago | parent [-]

> Sorry but I haven't really seen this pattern anywhere, care to give some examples?

As before, the standard library provides examples, including a "storage" example.

> All libraries that I recall ever using always export a single package

Right, and now there is questions around the language being spoken, so to speak. That's the power of idioms – it avoids the reader needing to ask new questions when encountering language that is new to them. But idioms are not the be all, end all. Sometimes they just don't fit, and if that's the case in your situation, no problem. Nobody knows your problems better than you. To listen to someone else tell you how to solve your own problems is foolish.

That is why I earlier wished we had seen some real code. Perhaps then we would understand the nuance that has lead to you choosing these particular tradeoffs. As a rule, though, an overarching interface package with consumptor functions and multiple implementation packages are preferable because then it avoids a lot of the questions developers are going to start asking.

For example, with the alternative suggested, what if I want to add a new storage adapter that conforms to your interface? Do I need commit rights to your package or should I create my own package that satisfies your exported interface? If I create my own package, why is it the lone implementation in its own package while the others are all rolled up in one package? Is it that you don't want third-party implementations for other services not covered by your package? Why is that? It goes on and on.

If you stick to the idioms, those questions are already answered through convention.

packetlost a day ago | parent | prev | next [-]

Rust and Go's lack of stack traces are basically equivalent in that you need to call an additional function to add the stack context to the error result. For go you use fmt.Errorf, in Rust you use .context from anyhow (bad practice in many contexts IMO) or .inspect_err + log. It's rather unfortunate that neither has an easy way of capturing a line number + file easily and appending it to the context. Go could easily do it, I think. Oh well.

I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do.

IMO Go should add really basic tagged unions and rework the stdlib to use them, but such a change would probably necessitate a v2.

miki123211 a day ago | parent | next [-]

> I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do.

There was a proposal for a `try`, which I still think should have been adopted.

Under that proposal, `someComplicatedExpression(try(functionReturningError()))` would be converted to `foo, err := functionReturningError(); if err != nil{return zeroValue, zeroValue, err}; someComplicatedExpression(foo)`

packetlost a day ago | parent [-]

You would need some form of compile-time reflection/specialization to implement that properly (what if the second return value isn't an error? What if there's only 1 return value?). Further, you would lose the ability to add context to the error branch via fmt.Errorf, which seems rather critical to understandable error conditions.

I'm not sure I would be satisfied with any implementation of try as the language is now, I assume the Go language team would probably feel the same.

dfawcus a day ago | parent | prev | next [-]

> I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do.

There is always the Odin style 'or_return' operator, which is defined for a similar situation.

https://odin-lang.org/docs/overview/#or_return-operator

pdimitar a day ago | parent | prev | next [-]

RE: Golang v2, they clearly said they will not do it and will double down on backwards compatibility with exceptions powered by env vars and/or CLI switches.

9rx a day ago | parent | next [-]

Technically, Go v2 signified the transition away from Google control to the project being directed by the community. That happened several years ago. Go v2 is already here and has been for a long time. The stdlib is also at v2 now (e.g. math/rand/v2).

You must mean the language? They said that a language v2 (go2) is probably unnecessary – that any future additions could be added without breaking the existing language. I don't expect simple tagged unions would need to break anything. A v2 (or even v3, perhaps) stdlib would be necessary to take advantage, like the parent suggests, but that has never been ruled out and is already the status quo.

packetlost a day ago | parent [-]

This was what I was referring to. The stdlib is what would need to see backwards-compatibility-breaking changes, not the language itself.

pdimitar 10 hours ago | parent [-]

I am not sure how would that work? F.ex. how would you introduce tagged unions and make the language 100% backwards-compatible... but not the stdlib?

I admit I have no idea.

9rx 8 hours ago | parent [-]

The stdlib would remain 100% backwards compatible, but the implication was that he would want to see certain existing features of the stdlib amended with modified versions that leverage the new tagged unions. He imagined that modification would necessitate v2 stdlib packages to maintain sensibility.

pdimitar 8 hours ago | parent [-]

Oh. Silly me, I am ashamed for failing reading comprehension so badly.

Thanks for clarifying, that makes sense.

packetlost a day ago | parent | prev [-]

I'm fully aware of that.

the_gipsy a day ago | parent | prev [-]

> Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types

It could be just some simple (hey, that's what go wants to be, right?) macro thing, that just does the everyday `if err!=nil{return ..., err}` for you without having to juggle (and think about) vars.

    b := g(f()?)?
    // var b B
    // {
    //     var a A
    //     var err error
    //     a, err = f()
    //     if err != nil {
    //         return *new(A), *new(B), err
    //     }
    //     var b B
    //     b, err = g(a)
    //     if err != nil {
    //         return *new(A), *new(B), err
    //     }
    //     // no accidental reuse of err
    // }
I mean look how much utterly useless noise this is, and count all the opportunities for mistakes that wouldn't get caught by the compiler.
dfawcus a day ago | parent | prev | next [-]

Alef actually has/had real tuples, as does Limbo.

Looking at Alef, apart from using tuples to provide multiple return values in a C-like language, they don't seem to actually add much functionality over what Go has without them. One of the few extra things is the ability to pass a tuple over a channel, however passing a struct is more or less equivalent.

I've not looked in much detail at what Limbo could do with its tuples.

So maybe they don't really add that much, hence why they were not carried over when Go was created?

Alef had enums, Limbo has something like the Go constant scheme and iota. Limbo also had tagged unions / varient-records, in a sort of Pascal like fashion - the "pick adt".

9rx a day ago | parent | prev | next [-]

> The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language

Go functions also accept a tuple on input. While theoretically you could pass an error, or a pointer to an error for assignment, it is a stretch to claim it is for errors.

the_gipsy a day ago | parent [-]

Yes exactly, that rather useless feature just makes the whole thing even weirder.

9rx a day ago | parent | next [-]

A function that passes tuples, or multiple arguments as they are more commonly referred to as, is not particularly weird, and has pretty much been the norm since the addition of functions to programming languages. But you are right that languages whose functions only accept tuples, but not return tuples, is a strange curiosity. In practice, you end up with developers packing multiple, unrelated items into a single variable to work around the limitation, which then questions why not do the same for input?

the_gipsy 21 hours ago | parent [-]

I meant weird/useless in the sense of that it's used virtually nowhere in practice.

There is a nice func Must(value, err) { if err { panic(err) } else { return value } that you can use in tests to get around the inane regular error handling. But that's about it, to my knowledge. Sad that there isn't more to it. If literally everything returns error as last return value, there could well be some syntax sugar there.

9rx 20 hours ago | parent [-]

I see it used often, even in languages that don't formally support multiple return arguments – with some hacked up array/object single value return to try and emulate what would be better represented as multiple return arguments.

Like I mentioned in another comment, Go does seem especially prone to attracting developers familiar with other languages who insist on trying to continue to program in those other languages even after working in a Go codebase. That may create a condition whereby the developers you regularly come across don't have a good grasp of where to use multiple return arguments effectively, and thus end up avoiding them, even where they would be appropriate.

With respect to the syntax sugar, there have been a number of proposals for exactly that. While the proposed syntax itself has been well received, nobody has come up with a good solution for the rest of the problem. Syntax is only about 10% of what is needed, of course. Rust, for example, has well defined traits and other features to go along with its '?' operator to fill in the remaining 90%. Presumably there is a good solution out there for Go too, but until someone proposes it...

the_gipsy 11 hours ago | parent [-]

The pragmatically "right" choice is to have some tuple type built-in. Because they are not only very good for function in/out, they can be used in many more places. Lists of tuples, tuples in structs, etc.

If the language doesn't have tuples, then you have to "roll your own" and emulate them every single time, but it's not a functor so you can't do all the useful stuff.

Go didn't do the pragmatically right choice, because it's only right from a type perspective. But go doesn't care about types at all, they're just a side effect. Go only needed to return multiple values, so that you don't have to pass in one (or more) "out" pointer as argument, and check an errnum. So go is right but only from its narrow perspective: it's better than C.

9rx 8 hours ago | parent [-]

> The pragmatically "right" choice is to have some tuple type built-in.

Perhaps, but in practice we end up with these ill-conceived languages that support tuples but end up not embracing them. Consider, for example, this Rust function:

   fn process_tuples(tuple1: (i32, i32), tuple2: (i32, i32), tuple3: (i32, i32))
If it made the "right" choice a function would only accept a single input value, which could be a tuple:

   fn process_tuples(tuples: ((i32, i32), (i32, i32), (i32, i32)))
But maybe that's not actually the "right" choice? What if there is a pragmatic reason for functions to have their own concept for tuples that is outside of a tuple type? I will admit, I would rather work with the former function even if it is theoretically unsound. It does a much better job of communicating intent, in my opinion.

And once you accept the theoretical unsoundness of a function having its own tuple concept for input, it naturally follows that it needs to also extend that concept to returns. After all, the most basic function is an identity function. What would be super weird is not being able to write an identity function any time the number of inputs is greater than one.

Go needed to allow multiple outputs because functions can accept multiple inputs.

the_gipsy 5 hours ago | parent [-]

> Go needed to allow multiple outputs because functions can accept multiple inputs.

I don't think that's the right conclusion, unless you have any source or insight?

It would explain why you can directly cast the multi-return-values into parameters when calling, but... that doesn't seem to fit go at all.

What I would think is that "go needed multi-return to be able to return errors along values", and the mentioned feature is just some vestige. It's not like go ever implements anything for the sake of consistency or correctness.

9rx 2 hours ago | parent [-]

> I don't think that's the right conclusion

It is the "right" conclusion in general. Multiple input arguments without multiple output arguments is frustratingly awkward. I can understand the appeal of your suggestion of only accepting one input and returning one output, where the passed value might be a tuple, but I expect there is good, pragmatic reason why virtually no languages, even those with a proper tuple type, haven't gone down that road. Once a language accepts multiple arguments, though, it needs them in and out. Otherwise you can't even write an identity function. If you can't write an identity function, is it even a function?

> "go needed multi-return to be able to return errors along values"

If there was some specific reason for its addition, beyond the simple fact that it would be crazy not to, it was no doubt for map presence checks. Without that, it is impossible to know if a map contains a given key. Errors can be dealt with in other ways. It wouldn't strictly be needed for that purpose, although it turns out that it also works for that purpose, along with a whole lot of other purposes. Not to mention that we know that the error type was a late-stage addition, so there is strong evidence that errors weren't a priority consideration in the language design.

the_gipsy 13 minutes ago | parent [-]

With conclusion I meant you claiming that go returns multiple values because a function also takes multiple values. I did not refer to judging this as worse or better than having tuples as general types instead.

> it was no doubt for map presence checks. Without that, it is impossible to know if a map contains a given key

Are you just making stuff up on the go? A map access is not even a function call. And if you need a presence check in the early language development stage, it's much simpler to add support to check presence and then get the value, since it's not threadsafe anyway.

a day ago | parent | prev [-]
[deleted]
yegle a day ago | parent | prev | next [-]

https://github.com/pkg/errors provides stack traces support. This unfortunately did not get included in the Go1.13 release when error wrapping was introduced.

devjab a day ago | parent | prev | next [-]

I prefer the structure of Rust errors as it’s fully typed, I don’t like that you can chain them though. It’s a cool feature but it leaves you with some of the same issues exception handling does when the freedom is used “wrong”.

KingOfCoders a day ago | parent | prev [-]

Exceptions are sum types, they just have different syntactic sugar.

9rx a day ago | parent | next [-]

Checked exceptions may be implemented as a sum type. Traditional exceptions are more likely to be a single type that wraps up a context object alongside stack trace information.

a day ago | parent | prev | next [-]
[deleted]
pema99 a day ago | parent | prev | next [-]

Not really. Exceptions usually imply unwinding the stack, and the ability to catch at any point throughout the callstack. Result types are just 'dead' data.

zozbot234 a day ago | parent [-]

These are fully equivalent in outcome, though often not low-level implementation. You can use try...catch (called panic...recover in Go) to pack a normal and abnormal return case into the equivalent of a Result<> type. Or just pass an abnormal Result<> back to the caller to manually unwind a single "layer" of the call stack.

biorach a day ago | parent [-]

> These are fully equivalent in outcome

They are so different in DX, ergonomics, implementation and traceability that I'm not sure this is true other than in the most abstract sense

sshine a day ago | parent [-]

There is some DX similarity between checked exceptions and Result types.

Because the compiler will fail if you don't explicitly mention each possible exception.

But checked exceptions are coming out of style: They're unchecked in C#, and frameworks like Spring Boot in Java catch all checked exceptions and rethrow them as Spring Boot flavored unchecked ones.

For unchecked exceptions and Result types:

The DX is very different in one critical way:

With Results you constantly have to differentiate between error and ok states, before you proceed. With unchecked exceptions you generally assume you're always in an ok state. It's equivalent to wrapping your whole function body in 'try { ... } catch (Exception e)'. And you can get that with Result types in Rust by using '?' and not worry about doing something half-way.

Ultimately: Are you a happy-path programmer?

Arnavion a day ago | parent [-]

>Because the compiler will fail if you don't explicitly mention each possible exception.

But only the first time. Once you add `throws FooException` to the caller signature, the compiler won't complain about any future callees that also happen to throw FooException, even if you did care about handling their exceptions yourself. With callees that return Result you do get to make that decision for every callee.

sshine an hour ago | parent [-]

That's a fair distinction.

I would say "adding Result to the caller signature" provides a similar cascade, but it's not entirely true: Every call must separately be unwrapped. So consistently wrapping your Result calls with the '?' operator is similar to a gigantic try-catch block or adding 'throws CheckedException' everywhere.

So both the '?' operator and adding 'throws CheckedException' everywhere let you accidentally neglect proper error handling: You have a default that is syntactically almost invisible and frees you from thinking. The checked exceptions give you a little more freedom from thinking than the '?' operator.

veidelis a day ago | parent | prev [-]

And different control flow, and different or sometimes non-existent types (Java's throws).

abtinf a day ago | parent | prev | next [-]

Having programmed for over 30 years, including nearly a decade of C#, I would say exceptions are one of the worst ideas in all of programming.

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.

mike_hearn a day ago | parent | next [-]

Well, I've also programmed for over thirty years and I wouldn't use a language without exceptions and even wrote a whole essay defending that position:

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.

bob1029 a day ago | parent | next [-]

> Diagnosing errors given stack traces is very easy.

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.

mike_hearn a day ago | parent [-]

Exception usability is definitely an area that needs work. If you work support for a dev platform for a while, it's a really common experience that people will post questions with a copy/pasted stack trace where the message+stack actually answers their question. You can just copy/paste parts back to them and they're happy. There's too much information and not enough tools to digest/simplify them and people get overwhelmed.

Still, better to have too much data than too little.

9rx a day ago | parent | prev | next [-]

> Useless: the server was written in Go and the logs had no information about where the error was originating. [...] In a language with exceptions there'd have been a stack trace that pinpointed exactly where the error originated

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

quotemstr a day ago | parent | prev [-]

I can't agree with you about C++ exceptions being worse than useless. Exceptional C++ is worth it. Safety isn't that hard with RAII and ScopeGuard..

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.

mike_hearn 10 hours ago | parent [-]

Haven't seen ScopeGuard before but it looks like an implementation of defer() in C++?

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.

quotemstr 2 hours ago | parent [-]

A significant number of C++ codebases use exceptions. Google famously doesn't, but Meta, alike in dignity, does. GDB, now C++, does. At least some AI labs do. C++ exceptions are normal and common.

rand_r a day ago | parent | prev | next [-]

> never handled correctly

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.

swiftcoder a day ago | parent [-]

Which could be solved in one swipe by adding a Result<T, Error> sum type, and a ? operator to the language. This is more a self-inflicted limitation of Go, then a general indictment of explicit error handling.

rand_r 18 hours ago | parent [-]

Nothing prevents explicit error handling in Python either. Forcing explicit error handling just creates verbosity since no system can functionally prevent you from ignoring errors.

kaoD a day ago | parent | prev | next [-]

Many people against Go's error handling do not advocate for exceptions, but for a combination of an Either/Result type (for recoverable errors) and fully aborting (for unrecoverable errors).

OtomotO a day ago | parent [-]

Abort on the other hand is used WAY to liberally in Rust.

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!

Chai-T-Rex a day ago | parent [-]

If a Rust function can panic, there's generally a non-panicking alternative. For example, `Vec` indexing has `vec[n]` as the panicking version and `vec.get(n)` as the version that can return `None` when there's nothing at that index.

mathw a day ago | parent | next [-]

I do wish this is something Rust had done better though - the panicking versions often look more attractive and obvious to developers, and that's the wrong way round. Vec indexing, IMO, should return Option<T>.

sshine a day ago | parent | next [-]

While that is true, there are clippy::indexing_slicing, clippy::string_slice for that:

  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_slicing
quotemstr a day ago | parent | prev [-]

One of my dream projects is creating a Rust stdlib based solely on panics for error handling. Sure, it'd be incompatible with everything, but that might be a feature, not a bug.

jll29 20 hours ago | parent | next [-]

One of my dreams is for someone to create a Rust stdlib, full stop.

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

OtomotO 18 hours ago | parent [-]

No!

I mean, yes, but not an official one.

The stdlib is where good ideas go to die.

Waiting for "pub struct RDBMSInferfaceV17ThisTimeWeGotItRightForSure"

thesuperbigfrog 15 hours ago | parent [-]

Firm disagree.

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.

andrewshadura a day ago | parent | prev [-]

I think someone's already created this.

OtomotO a day ago | parent | prev [-]

in the stdlib, yes. In 3rd party crates? Depends!

OtomotO a day ago | parent | prev | next [-]

But with Exceptions you can easily implement multiple return types in e.g. Java ;)

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

Cthulhu_ a day ago | parent | next [-]

As you should, it shows a deeper insight in the language beyond the base course material and out of the box (and cursed) thinking.

Yoric a day ago | parent | prev | next [-]

In OCaml, that's actually a common use for exceptions, as they let you unwind extremely quickly and carry a result (helped by the fact that OCaml doesn't have finally or destructors).

sshine a day ago | parent | prev [-]

When I took the compiler course at university, the professor would have a new coursework theme every year, and the year I took the course, the coursework compiler was exception-oriented. So exceptions were the only control flow mechanism besides function calls. If/else, while, return were all variations of throw.

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.

BoiledCabbage a day ago | parent [-]

Sounds interesting - any link to the control flow implementation online?

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?

pyrale a day ago | parent | prev | next [-]

Exception and explicit on-the-spot handling are not the only two ways to handle failing processes. Optional/result types wrapping the are a clean way to let devs handle errors, for instance, and chaining operations on them without handling errors at every step is pretty ergonomic.

quotemstr a day ago | parent [-]

Rust's error handling evolution is hilarious. In the beginning, the language designers threw out exceptions --- mostly, I think, because Go was fashionable at the time. Then, slowly, Rust evolved various forms of syntactic sugar that transformed its explicit error returns into something reminiscent of exceptions.

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.

chrismorgan a day ago | parent | next [-]

One practical benefit of Rust’s approach that hasn’t been emphasised enough yet is the consequences of Option<T> and Result<T, E> being just values, same as anything else.

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.

quotemstr a day ago | parent [-]

Exceptions are values in C++, Java, and Python too. They're just values you throw. You can program these values.

As usual, I find that opposition to exceptions is rooted in a misunderstanding of what exceptions really are

chrismorgan a day ago | parent [-]

Exceptions are values, but normal-value-or-exception (which is what Result<T, E> is) isn’t a value. Review my remarks about map_err, ok_or, &c. with the understanding that Result is handling both branches of the control flow, and you can work with both together, and you might be able to see it a bit more. Try looking at real code bases using these things, with things like heavily method-chained APIs (popular in JS, but exceptions ruin the entire thing, so such APIs in JS tend to just drop errors!). And try to imagine how the collect() forms I described could work, in an exceptions world: it can’t, elegantly; not in the slightest.

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.

maleldil a day ago | parent | prev | next [-]

> what's the difference between your program and one with exceptions

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.

quotemstr a day ago | parent [-]

No, because you end up with a function coloring problem that way. A function that returns something other than Result has to either call only infallible code or panic on error, and since something can go wrong in most code, the whole codebase converges as time goes to infinity on having Result everywhere.

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.

dwattttt a day ago | parent | next [-]

I very much care about whether a function can fail or not, and I encourage all the function colouring needed to convey that.

funcDropShadow a day ago | parent [-]

As almost always, we programmers / software developers / engineers, forget to state our assumptions.

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.

maleldil 4 hours ago | parent [-]

Monadic error handling (such as Rust's) is a good compromise because it allows you to handle errors precisely when you want to _and_ bubble them up with minor boilerplate when you don't. You can even add context without unwrapping the whole thing.

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

Too a day ago | parent | prev | next [-]

I think your conclusion is on the right track. Syntax sugar propagating results is in a way equivalent to exceptions. What you are missing it isn’t just equivalent to ordinary exceptions. It’s more equivalent to checked exceptions. A very powerful concept, that unfortunately got a bad rap, because the most widespread implementation of it (Java) was unreasonably verbose to use in practice. You might claim Rust is still too verbose and I don’t disagree with that, it’s a big improvement from Java at least, while at the same time providing even more safety nets.

jll29 20 hours ago | parent [-]

Yes, Java is too verbose, but Kotlins cleaned up much of that boilerplate and runs on the same VM.

I'd be curious to see examples for where you think Rust is still to verbose.

maleldil 4 hours ago | parent [-]

You misunderstood. They meant that Java's checked exceptions are very verbose and that Rust's approach to errors is similar. While Rust's approach is less verbose, it's still more verbose than "regular" (unchecked exceptions).

Kotlin isn't relevant here because all exceptions are unchecked.

high_na_euv 6 hours ago | parent | prev | next [-]

"Function coloring problem"

Function coloring isnt a problem, it is just approach

sunshowers a day ago | parent | prev | next [-]

Function "coloring" is good! It's not a problem here and it's overblown as a problem in general. If something fails recoverably then it should be indicated as such.

pyrale a day ago | parent | prev [-]

> A function that returns something other than Result has to either call only infallible code or panic on error

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

quotemstr a day ago | parent [-]

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

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.

ViewTrick1002 a day ago | parent [-]

In my experience an unwrap, expect or panicking function is a direct comment in code review and won’t be merged without a reason explaining why panicking is acceptable.

zozbot234 a day ago | parent | prev | next [-]

The '?' operator is the opposite of a footgun. The whole point of it is to be very explicit that the function call can potentially fail, in which case the error is propagated back to the caller. You can always choose to do something different by using Rust's extensive facilities for handling "Result" types instead of, or in addition to, using '?'.

pwdisswordfishz a day ago | parent | prev | next [-]

In most languages with exceptions:

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

quotemstr a day ago | parent [-]

> they may propagate automatically from any point in code, potentially breaking atomicity invariants and preventing forward progress

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.

Ygg2 20 hours ago | parent [-]

> A failure can propagate in the same circumstances in a Rust program.

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.

mottalli a day ago | parent | prev | next [-]

Honest question: syntactically noisy as opposed to what? In the context of this post, which is a critique of Go as a programming language, for me this is orders of magnitude better than the "if err != nil {" approach of Go.

pyrale a day ago | parent | prev | next [-]

I'm not sure why you bring up Rust here, plenty of libs/languages use the Result pattern.

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.

sunshowers a day ago | parent | prev | next [-]

The big difference is API stability, one of the primary focuses of Rust.

Ygg2 a day ago | parent | prev [-]

> Once every return is a Result, every call a ?, and every error a yeet

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.

bazoom42 a day ago | parent | prev | next [-]

Is “goto” just used to mean “bad and evil” here? Because exceptions are not a goto anymore than a return is a goto. The problem with goto is it can jump to any arbitrary place in your code. Exceptions will only go to catch-blocks up the call stack, which presumably you have written on purpose.

vaylian a day ago | parent [-]

> Because exceptions are not a goto anymore than a return is a goto

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

abtinf 20 hours ago | parent [-]

Very well put.

Seb-C a day ago | parent | prev | next [-]

I agree about implicit exceptions, but I think that there is a sweet spot with explicit exceptions like Swift (and maybe Java): where you cannot not-handle one, it is part of a function's signature, and the syntax is still compact enough that it does not hurt readability.

therealdrag0 14 hours ago | parent [-]

I think checked exceptions are noise. They usually never provide value, and are often just caught and rethrown as runtime exceptions to avoid the boilerplate. Scala did right to remove them.

quotemstr a day ago | parent | prev [-]

Hard disagree. Exceptions are actually good. They make code clear and errors hard to ignore. I've written a ton of code over decades in both exceptional and explicit-error languages and I'll take the former every day. There's no function color problem. No syntactic pollution of logic with repetitive error propagation tokens.

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.

maleldil a day ago | parent | next [-]

> Have you read any real world Rust code? Junior developers just add unwrap() until things compile.

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.

[1] https://blog.burntsushi.net/unwrap/

OtomotO a day ago | parent | prev | next [-]

Bah, no, I hated that you had to wrap basically every code block in a try/catch in Java, because the underlying lib could change and suddenly throw a Runtime-Exception.

At the same time Checked Exceptions were a nightmare as well, because suddenly they were part of the contract, even though maybe wrong later.

didntcheck a day ago | parent | next [-]

> the underlying lib could change and suddenly throw a Runtime-Exception.

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?

OtomotO a day ago | parent [-]

Always depends on which level it happens

quotemstr a day ago | parent | prev [-]

Checked exceptions are more trouble than they're worth. That doesn't make exceptions in general bad.

mathw a day ago | parent | next [-]

Not having checked exceptions is a huge problem, because then you never know when something might throw and what it might through, and in the .NET world the documentation on that is pretty awful and absolutely incomplete.

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

neonsunset a day ago | parent [-]

> in the .NET world the documentation on that is pretty awful and absolutely incomplete.

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.

Cthulhu_ a day ago | parent | prev | next [-]

What does make exceptions bad in my opinion (and shared by Go developers?) is a few things:

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.

cesarb a day ago | parent [-]

> e.g. files that don't exist, database rows that don't exist, etc. [...] The workaround is defensive coding, check if the file exists first, check if the row exists?

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.

OtomotO a day ago | parent [-]

Totally agreed, but as the previous poster wrote:

an exception is meant for EXCEPTIONAL behavior.

So it may be that the file access throws an exception but generally, I wouldn't agree.

OtomotO a day ago | parent | prev [-]

As said, I don't like the wrapping of about everything with try/catch

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.

biorach a day ago | parent | prev [-]

> Oh, and sum types? Have you read any real world Rust code? Junior developers just add unwrap() until things compile.

Junior developers will write suboptimal code in any language. So I'm not sure what your point is.

stouset a day ago | parent | prev | next [-]

Without fail, every single person I’ve seen rave about go’s error handling compares it only to exceptions as if that’s the only alternative.

On the flip side I have yet to find a person who’s familiar with sum types (e.g., Maybe, Option, Result) that finds the golang approach even remotely acceptable.

OtomotO a day ago | parent | next [-]

Here, you've found me.

I don't LIKE it, but it's acceptable.

I have been working with Rust since 2015 (albeit not professionally, but a lot of side projects) and love it.

But I also dabbled into go the last couple of months and while it has its warts, I see it as another tool in the tool-belt with different trade-offs.

Error handling is weird, but it's working, so shrug

cnity a day ago | parent [-]

Sorry, balanced opinions are not welcome in discussions about favourite programming languages.

clausecker a day ago | parent | prev | next [-]

I dislike sum-type based error handling. It is annoying syntactically and only really doable with lots of high-level combinators, which in turn hinder debuggability.

alper 8 hours ago | parent | next [-]

I'm dealing with Rust based error handling now and nesting `match` statements is not exactly very happy. Or having to pull in `anyhow` and dealing with its awful awful documentation.

lionkor a day ago | parent | prev [-]

Have you tried the approach that Zig has, or the approach that Rust has? They are easy to debug and do not use any crazy stuff, just simple syntax like `try x()` (Zig) or `x()?` (Rust)

clausecker 21 hours ago | parent [-]

Yes and that syntax sucks.

lionkor 13 hours ago | parent [-]

That sounds like your opinion rather than a "it sucks because it encouraged bad practices" or something

devjab a day ago | parent | prev | next [-]

I think it’s because Go is an alternative to Java and C# more so than an alternative to Rust. It is for me at least. As I said, Rust isn’t seeing any form of real world adoption in my region while Go is. Go isn’t replacing C/C++ or even Python though, it’s replacing Typescript, C# and Java. Now, there are a lot of good reasons as to why Go shouldn’t be doing that, a lot of them listed in the article, but that’s still what is happening.

As I pointed out I think Rust does it better with its types error handling. That isn’t too relevant for me though as Rust will probably never seem any form of adoption in my part of the world. I think Zig may have a better chance considering how interoperable it is with C, but around here the C++ folks are simply sticking with C++.

zozbot234 a day ago | parent [-]

> Now, there are a lot of good reasons as to why Go shouldn’t be doing that

I disagree. Typescript, C# and Java are terrible languages (as are Python/Ruby/etc. in other ways). Golang is bad by OP's standards but there's nothing wrong with it gaining ground on those languages.

Besides it's also easier to convert a codebase to Rust from Golang than Typescript or C#/Java.

high_na_euv 6 hours ago | parent | next [-]

C# is the best designed lang out of the top10 most popular langs

devjab a day ago | parent | prev | next [-]

It was meant more as an observation than my opinion. I would pick Go over Java/C# any day of the week, but it’s not like talented JVM engineers won’t run in circles around you as far as performance goes.

I’d frankly pick Python for most things though. It’s a terrible language, everyone knows it’s terrible but it gets things done and everyone can work with it. I view performance issues a little different than most people though. To me hitting the wall where you can no longer “make do” with C/Zig replacements of Python bottlenecks means you’ve made it. The vast majority of software projects will never be successful enough to get there.

neonsunset a day ago | parent | prev [-]

Rust and C# have far more overlap than Go could ever hope for. Go is limited (and convoluted sometimes due to "solutions" devised to cope with it) so it is easily expressible in languages with better type systems and concurrency primitives.

DanielHB a day ago | parent | prev | next [-]

You hit the main gripe I have with Go, its types system is so basic. I get people raving type-correctness of Go when they come from Python but the type system in Go is simply pre-historic by modern day standards.

masklinn a day ago | parent | next [-]

Go’s type system is not even impressive compared to python’s.

orwin a day ago | parent | next [-]

Do you have a pydantic equivalent in go? Also modern typing in python is starting to be OK to be honest (well, if you consider typescript typing OK), so it isn't really a knock on Go :)

Yoric a day ago | parent [-]

> Do you have a pydantic equivalent in go?

I've been working on one [1].

But gosh, does go make it hard.

[1] https://github.com/pasqal-io/godasse

DanielHB a day ago | parent | prev [-]

Well I was comparing to python codebases before they added type annotations

Yoric a day ago | parent [-]

Which, sadly, is still the case of too many dependencies.

While I much prefer Python as a language, Go wins against Python by having a fresher ecosystem, with a higher baseline for type safety. Still pretty low with respect to Rust or mypy/pyright with highest settings, but much better than any of the Python frameworks I've had to deal with.

yyyfb a day ago | parent | prev [-]

I feel that the future for Python people who want type safety will eventually be TypeScript on nodejs. Go was intended as an alternative to C++. It seems that in reaction to the ungodly complexity of C++, the creators wanted to avoid adding language features as hard as possible. If the user could work around it with a little extra verbosity, it'd be ok. I feel they removed too much and maybe not the right things.

eska a day ago | parent | prev | next [-]

That’s to be expected since it is marketed towards beginner and casual programmers.

Cthulhu_ a day ago | parent [-]

I don't agree that that's what it's marketed towards, but it was designed with those in mind. That said, experienced developers can enjoy it too, as code is just a means to an end and code complexity or cleverness does not make for good software in the broader sense of the word.

It's a Google solution to Google scale problems, e.g. codebases with millions of lines of code worked on by thousands of developers. Problems that few people that have an Opinion on Go will ever encounter.

euroderf a day ago | parent | prev [-]

FWIW... WebAssembly has Option and Result, and adapters for Go.

cnity a day ago | parent [-]

What do you mean by this? WebAssembly is a low level bytecode which only defines low level types. WebAssembly doesn't "have" types any more than x86 "has" types right? Or have I missed something?

euroderf a day ago | parent [-]

Ah, sorry. Shoulda said: WebAssembly component model.

cnity a day ago | parent [-]

Oh, I see. Thanks!

cowl a day ago | parent | prev | next [-]

this part of error handling is pure religion. it goes even against one of the most basic go tenents. that code should be easy to read not write. Try reading and understanding the logic of a particular method where 75% of the lines are error noise and only 25% are the ones you need to understand what the method does. yes it's noise because whenever read a codebase for the first time you are never interested on the the error edge case. first glance readability needs to tell you what you are trying to accomplish and only after what you are doing to make sure that is correct.

on this point go's error handling is a massive fail. Notice that I'm not saying explicit error handling is bad. I'm saying the insistence that error handling needs to be implemented inline interleaved with the happy path is the problem. You can have explicit error handling in dedicated error handling sections

majormajor a day ago | parent | next [-]

> yes it's noise because whenever read a codebase for the first time you are never interested on the the error edge case.

maybe this has something to do with how bug-prone it usually is for a new hire to modify a codebase for the first time in most orgs

you could also just do things fail-fast style and panic everywhere if you REALLY wanted to stop inlining error conditions. or ignore error checks until some sort of guard layer that validates things.

IME you usually don't want to do either of those things, and the go approach at least encourages you to think about it closer to the site than checked exceptions (which you can more easily toss up and up and up and auto-add to signatures of callers). unchecked exceptions are arguably less-bad than "just ignore go return errors" - they'll get seen! - but terrible for a reliability/UX perspective.

optional-esque approaches are nice but just a different flavor of the same overhead IMO.

Yoric a day ago | parent | prev [-]

Do you have examples for the latter?

cowl a day ago | parent [-]

the most basic example was the declined proposal https://github.com/golang/proposal/blob/master/design/32437-...

Some people didn't like the "try" keyword it reminded them too much of exceptions, some people didn't like that they couldnt see a return inline (which was the purpose of the proposal in the first place).

it's not that there are no solutions. the main problem is the go team's insistence to have "one true way" (tm) of doing something and unfortunately this gap between people who want to see every return inline and people who want to see the clean solution separate from the error handling is not something that can be bridged by technical means. the only solution is to implement both ways and lets see which one wins.

Yoric a day ago | parent [-]

This doesn't look meaningfully different from current error handling in Go.

It's basically the same syntactic sugar as `try!` in Rust, isn't it?

monksy a day ago | parent | prev | next [-]

I would certainly argue against the claim that explicit error handling is far overkill.

Where I agree: It forces you to think about all of the possibilities your code might generate. (This is more of a C question than it is with other languages)

However, when abstracting blocks of code away, you don't always need to handle the error immediently or you may want to handle it down the stack.

You're giving up a lot of readability in order for the language to be particular.

madeofpalk a day ago | parent | next [-]

> It forces you to think about all of the possibilities your code might generate.

Except it doesn't actually. You can totally just ignore it and pretend errors don't exist. Lack of sum types/Result, and pointers as poor mans optional, really hinder's Go's error handling story.

monksy a day ago | parent [-]

I agree with you that the sum types are much better and it's intentional that they wanted null support. (Which was a frustrating pick considering the modern dev languages out there).

The coding style encourages the behavior you're talking about. We're back in the C days where you have a result and an error. It's not a good pattern.

acdha a day ago | parent | prev | next [-]

> It forces you to think about all of the possibilities your code might generate

I’ve seen way too much Go code which never even tested the err value to believe that until something like errcheck is built in to the compiler.

I do agree that this is a plus for the explicit model but that’s been a better argument for Rust in my experience since there’s better culture and tooling around actually checking errors. I’m sure there are plenty of teams doing a good job here, but it always felt like the one lesson from C they didn’t learn well enough, probably because a language created by experts working at a place with a strong culture of review doesn’t really account for the other 99% of developers.

eru a day ago | parent | prev [-]

Rust handles this much better.

Error handling is still explicit, but it gives you the tools needed to make it less tedious.

Attummm a day ago | parent | prev | next [-]

For me, the issue with error handling is that while errors are explicitly stated, they are often poorly handled. Rarely have I seen the handling of multiple reasons for why an error might occur, along with tailored approaches to handle each case. This is something very common in older languages like Python or Java

cnity a day ago | parent | next [-]

As a regular Go user, I agree with this take. Though the tools exist, error wrapping and checking (with errors.Is and so on) is actually pretty rare in my experience.

Positive example of good and appropriate usage here: https://github.com/coder/websocket/blob/master/internal/exam...

Cthulhu_ a day ago | parent | prev [-]

This is down to developer style and agreements though; Go has typed errors and a set of utilities to match them [0]. Not using those is a choice, just like how in Java you can just `catch (Exception e)` after calling a dozen methods that might each throw a different exception.

[0] https://pkg.go.dev/errors

Yoric a day ago | parent [-]

Interestingly, every time (and I mean _every_ time) that I've tried to use `errors.As` on errors raised by lib code, I found out that the lib just went with "nah, I'm just going to use `errors.New` or `fmt.Errorf`", which makes the error impossible to match.

So... I'd say that this is a fumble in the design of Go.

wbl a day ago | parent [-]

%W exists to solve this

Yoric 7 hours ago | parent | next [-]

It would, if people actually defined new errors. But, as I mentioned in my message, people just use `errors.New` or `fmt.Errorf`, all the way down, so you end up with errors that cannot be matched. Which means, in turn, that if these are errors that should be handled properly (and not just propagated/logged), they cannot.

the_gipsy 21 hours ago | parent | prev [-]

How? Stringly matching? That's not typesafe at all.

wbl 17 hours ago | parent [-]

No it wraps the underlying error

the_gipsy 9 hours ago | parent [-]

But the underlying error stays unmatchable. Doesn't sound like a solution if you have to duplicate every error type, and worse, they don't even map 1:1 but now you have the same underlying error wrapped to god knows how many different errors.

For example, the lib produces some an error "bad file descriptor". You'll be wrapping it when you call fileOpen, fileDelete, etc etc 20 times. So you will be wrapping it in "open error", "delete error", etc, 20 times. You cannot try to match it to "bad file descriptor", that information is lost, you now have 20 relatively useless errors.

Except if you stringly match.

wbl 5 hours ago | parent [-]

https://pkg.go.dev/errors#Is and https://pkg.go.dev/fmt#Errorf clearly state that there is a way to match these errors if the package exposes the values, which the stdlib does.

the_gipsy 28 minutes ago | parent [-]

Ah okay, it seems to work because errors are pointers, and errors.Is just checks for equality.

flir a day ago | parent | prev | next [-]

I see a lot of people say this about exceptions, and I don't have that problem. The exception bubbles up the stack until something catches it. Ok it's a different code path, but it's a very simple one (straight up). So you either catch the exception nearby and do something specific with it, or it bubbles up to a generic "I'm sorry there was a problem please try again later" handler.

Honestly makes me wonder what I'm missing. Maybe it's because I don't deal with state much? Do the problems start to mount up when you get into writing transaction locks, rollbacks etc? But I don't see why you wouldn't have the same problems with Go's mechanism.

Hoping to gain enlightenment here.

[copied from a comment below]: 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.

Maybe this is it? I prefer a "fail early and often" approach, and I tend to validate my data before I perform operations on it. I don't want to attempt recovery, I want to spew log messages and quit.

uzerfcwn 20 hours ago | parent | next [-]

Exceptions are difficult to discuss because different languages implement exceptions differently, each with their own downsides. That said, I don't think anyone has an issue with bubbling. Even sum type proponents love Rust's ? shorthand, because it makes it easier to propagate Results up the stack.

The big issue with exceptions in C#, Python and JS is that they're not included in function signatures, which means you have to look up the list of possible exceptions from documentation or source code. This could be amended with checked exceptions like Java, but it allegedly doesn't mesh well with the type system (I haven't personally written Java to confirm this). And then there's the C++ crowd that slaps noexcept on everything for possible performance gains.

Personally, I like the way Koka does exceptions with algebraic effects and type inference. It makes exceptions explicit in function signatures but I don't have to rewrite all the return types (like in Rust) because type inference takes care of all that. It also meshes beautifully with the type system, and the same effect system also generalizes to async, generators, forking and other stuff. Alas, Koka is but a research language, so I still write C# for a living.

acdha a day ago | parent | prev [-]

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

I think that’s misdirected but illustrates the emotional reasons why people develop a negative impression of the concept. Usually it means someone had bad experiences with code written in a poor culture of error handling (e.g. certain Java frameworks) and generalized it to “exceptions are bad” rather than recognizing that error handling isn’t trivial and many programmers don’t take it seriously enough, regardless of the paradigm. As a simple example, C and PHP code have had many, many security and correctness issues caused by _not_ having errors interrupt program execution where the users would have been much better off had the program simply halted on the first unhandled error.

If you write complex programs with lots of mutable shared state, yes, it’s hard to reason about error recovery but that’s misattributing the problem to the mechanism which surfaced the error rather than the fact that their program’s architecture makes it hard to rollback or recover.

pansa2 a day ago | parent | prev | next [-]

> Go is seeing adoption that no other “new” language has exactly because of its simplicity

Yes - for me, the simplicity is essential. As a part-time programmer, I don't have months to spend learning C++ or Rust.

If my project needs to compile to small(-ish) standalone binaries for multiple platforms (ruling out Python, Ruby, Java, C#, etc) what simple alternative language is there? Plain C?

martindevans a day ago | parent | next [-]

C# can compile standalone binaries for multiple platforms.

acdha a day ago | parent | prev [-]

Basic Rust doesn’t take months to learn, especially when you’re not trying to do things like distributing crates to other people. I found the compiler to be enough more helpful than Go’s to make them roughly time-equivalent for the subset of common features, especially for a simple CLI tool.

damnever 16 hours ago | parent | prev | next [-]

Go's error handling is not explicit because error is an interface. So, a nil MyError is not the same as a nil error, and error comparison is inconvenient because the interface constraints are so loose that people often resort to checking errors with strings.Contains.

troupo a day ago | parent | prev | next [-]

It's actually very rare that it should be the caller who has to handle the errors.

Go, however, forces you to spread your error handling over a thousand little pieces with zero overview or control of what's happening.

Rust eventually realised this and introduced try! and ? to simplify this

koito17 a day ago | parent | next [-]

More importantly, Rust has the notion of a result type and it is designed to be both generic and composable.

A problem I often face in Go and TypeScript code is code that ignores errors, often unintentionally. For instance, many uses of JSON.parse in TypeScript do not check for the SyntaxError that may be thrown. In Go, it is common to see patterns like

  _ := foo.Bar()
  // assume Bar() returns error
This pattern exists to tell the reader "I don't care if this method returns an error". It allows one to avoid returning an error, but it also stops the caller from ever being handle to the error.

Also, the position of the error matters. While the convention in the stdlib is to return errors as the final value, this isn't necessarily followed by third party code.

Similarly, errors are just an interface and there is no requirement to actually handle returned errors. Even if one wants to handle errors, it's quite awkward having to use errors.As or errors.Is to look into a (possibly wrapped) chain of errors.

The benefit of Rust's Result<T, E> is that

- position doesn't matter

- there is strong, static type checking

- the language provides operators like ? to effortlessly pass errors up the call stack, and

- the language provides pattern matching, so it's easy to exhaustively handle errors in a Result

The last two points are extremely important. It's what prevents boilerplate like

  if err != nil {
    return nil, err
  }
and it's what allows one to write type-safe code rather than guess whether errors.As() or errors.Is() should be used to handle a returned error.
DanielHB a day ago | parent | next [-]

I am pretty sure if it were for the Typescript creators they would not allow exceptions in the language, but they had to work within the confines of Javascript. Heck they even refused to make exceptions part of the type-system.

It is unfortunate that many of Typescript developers still rely on throwing exceptions around (even in their own typescript code). Result types are totally doable in Typescript and you can always wrap native calls to return result types.

quotemstr a day ago | parent | prev [-]

Why would you "check" for TypeError being thrown? Just let exceptions in general propagate until they reach one of the few places in the program that can log, display, crash, or otherwise handle an exception. No need to "check" anything at call sites.

90% of the criticism of exceptions I see comes from the bizarre and mistaken idea that every call needs to be wrapped in a try block and every possible error mentioned.

Yoric a day ago | parent | prev | next [-]

> Rust eventually realised this and introduced try! and ? to simplify this

That was prototyped around Rust 0.4, so I wouldn't say "eventually" :)

liotier a day ago | parent | prev [-]

Unsure if this is the right place to ask, but this conversation inspires me this question:

Is there in practice a significant difference between try/catch and Go's "if err" ? Both seem to achieve the same purpose, though try/catch can cover a whole bunch of logic rather than a single function. Is that the only difference ?

slau a day ago | parent | next [-]

Try/catch can bubble through multiple layers. You can decide/design where to handle the errors. If you don't `if err` in Golang, the error is skipped/forgotten, with no way to catch it higher up.

theshrike79 a day ago | parent | prev | next [-]

You can decide not to catch a thrown exception, it travels upwards automatically if you don't catch it.

I think that's the biggest difference.

With Go you need to specifically check the errors and intentionally decide what to do, do you handle it right there or do you bubble it upwards. If you do, what kind of context would the caller want from this piece of code, you can add that too.

stouset a day ago | parent | next [-]

> With Go you need to specifically check the errors and intentionally decide what to do, do you handle it right there or do you bubble it upwards.

Is this really all that interesting or worth the LOC spent on error handling when 99.9999% of the time in practice it’s just bubbled up?

And any “context” added is just string wrapping. Approximately nobody types golang errors in a way that lets you programmatically know what went wrong, to be able to fix it in-line.

I think I would be more empathetic to the arguments defending golang here if I’d ever worked or seen a project where people actually handled errors instead of spending 2/3 of their time writing code that just punts on any error.

LinXitoW a day ago | parent | prev [-]

I'd argue that at least checked exceptions also require a conscious decision from you. You either need to add the Exception type to your throws clause, or your catch clause.

Compared to Go, this is actually better because the type system can tell you what kind of errors to expect, instead of just "error".

Too a day ago | parent | prev [-]

“if err” doesn’t catch all types of errors. Some errors are colored different from others and instead cause the program to immediately crash, sorry panic. But don’t worry! it’s just very rare errors, like nil dereference and index out of bounds, that throw unrecoverable errors like this!

XorNot a day ago | parent | prev | next [-]

When I started trying to teach myself Rust, the error handling story fell apart on me very quick.

Like as soon as I wanted to try and get sensible reporting in their, suddenly we were relieving libraries, adding shims and fighting mismatched types and every article was saying the same thing: haha yeah it's kind of a problem.

I'm very, very unsold on explicit error handling compared to exceptions for practical programming. The number of things which can error in a program is far larger then those that can't.

lkirkwood a day ago | parent | next [-]

I felt the same but after switching to anyhow and thiserror in pretty much every Rust project I work on I find it quite painless. It's not ideal to rely on crates for a core language feature but I never find myself fighting error types anymore. Have you tried those crates? Do you still hold that opinion?

masklinn a day ago | parent [-]

You don’t need crates for it, anyhow is basically a better Box<dyn Error>, if you just want the error signal you can use that. The main thing missing from the stdlib fur this use case is I don’t think there’s anything to easily wrap / contextualise errors built in.

usrnm a day ago | parent | prev [-]

The problems you're describing don't exist in go. There is exactly one standard type that is used by everyone, at least in public API's, you can always just return the error to the caller, if you don't want to handle it in place. The main difference with exceptions in my practice is the fact that it's a lot easier to mess up, since it requires manual typing. This is probably my main problem with everything being as explicit as possible: it requires people to not make mistakes while performing boring manual tasks. What could possibly go wrong?

Yoric a day ago | parent | next [-]

The drawback, on the other hand, is that all the Go code I've read (including the stdlib and all the dependencies of my current project) is using `fmt.Errorf` or `errors.New`, which means that you can't use `errors.As`, which means that you generally cannot handle errors at all.

XorNot a day ago | parent | prev [-]

I think that sort of nails it: the problem with errors as values is errors become part of the type signature and put under user control, and the user can't really be trusted with that power.

Even the simplest functions tend to have error states - i.e. floating point math can always wind up handing back NaN.

So where I end up is, the default assumption is every function is effectively of a type MightError(T)...so why even make us type this? Why not just assume it, assume the unwrap handling code, and so you basically wind up back at try-catch exception handling as a syntactic sugar for that whole system.

pif a day ago | parent | prev [-]

> I’ve previously spoken about my loathing of exception handling because it adds a “magic” layer to things which is way too easy to mess up.

I kind of see your point. In this very moment, it doesn't matter whether I agree. What I don't understand, though, is why (typically) people who abhor exceptions are among the fiercest defenders of garbage collection, which does add a “magic” and uncontrollable layer to object destruction.

Personally, having learned to love RAII with C++, I was shocked to discover that other languages discarded it initially and had to add it in later when they realized that their target developers are not as dummy as those choosing Golang.

Mawr a day ago | parent | next [-]

Different kind of magic. Needing to account for every single line of code being able to throw an exception is very mentally taxing, whereas the existence of a GC removes mental load of needing to account for every single allocation.

bsaul a day ago | parent | prev [-]

How does RAII works in concurrent systems ? It seems to me you need to add compile-time object lifetime evaluation (as in rust) which so far incurs a high toll on language complexity.

maleldil a day ago | parent [-]

The exact same way it works in Rust. C++'s RAII works the same as Rust's Drop trait. The object is released when it goes out of scope, and if it's shared (e.g. Arc), it's released when the last reference to it drops.