←back to thread

257 points pmig | 8 comments | | HN request time: 0.001s | source | bottom
Show context
blindriver ◴[] No.43096757[source]
I've been using Go for a while now. The biggest headache is error handling. I don't care what the "experts" say, having exception handling is so, so, so much cleaner in terms of error handling. Checking for err is simply bad, and trickling errors back up the call stack is one of the dumbest experiences in programming I've endured in multi-decades. If they are willing to add generics, they should add exception handling as well.
replies(4): >>43097025 #>>43097105 #>>43097710 #>>43098949 #
bmurphy1976 ◴[] No.43097105[source]
To each their own. I'm not going to claim to be an expert, but as somebody who's been coding since the 80s it was a breath of fresh air to see Go do what I wanted languages to do all long instead of ramming exceptions down my throat. I have problems with Go (examples: slice behavior and nil type interfaces) but error handling is not one of them.
replies(1): >>43097199 #
CharlieDigital ◴[] No.43097199[source]
What challenge did you run into with exception handling?

I'm curious because I've never felt it being onerous nor felt like there was much friction. Perhaps because I've primarily built web applications and web APIs, it's very common to simply let the exception bubble up to global middleware and handle it at a single point (log, wrap/transform). Then most of the code doesn't really care about exceptions.

The only case where I might add explicit exception handling probably falls into a handful of use cases when there is a desire to a) retry, b) log some local data at the site of failure, c) perform earlier transform before rethrowing up, d) some cleanup, e) discard/ignore it because the exception doesn't matter.

replies(6): >>43097659 #>>43098169 #>>43098430 #>>43099302 #>>43100129 #>>43101696 #
ratorx ◴[] No.43097659[source]
In Go, b) is really common. Most of my code will annotate a lower error with the context of the operation that was happening. You’ll ideally see errors at the top level like: “failed to process item ‘foo’: unable to open user database at ‘/some/path’: file does not exist” as an example.

Here, the lowest level IO error (which could be quite unhelpful, because at best it can tell you the name of the file, but not WHY it’s being opened) is wrapped with the exact type of the file being opened (a user database) and why the database is being opened (some part of processing ‘foo’, could even generate better error message here).

Although this is a bit of work (but in the grand scheme of things, not that much), it generates much better debugging info than a stack trace in a lot of situations, especially for non-transient errors because you can annotate things with method arguments.

I think the common complaint of ‘if err != nil { return err }’ is generally not the case because well-written Go will usually prepend context to why the operation was being performed.

replies(3): >>43097748 #>>43097780 #>>43098313 #
1. throwaway2037 ◴[] No.43098313[source]

    > Most of my code will annotate a lower error with the context of the operation that was happening.
This is easy to solve with chained exceptions to add context.

    > it generates much better debugging info than a stack trace in a lot of situations, especially for non-transient errors because you can annotate things with method arguments.
You cannot add method args to an exception message? I am confused.
replies(2): >>43100887 #>>43101880 #
2. ratorx ◴[] No.43100887[source]
It is definitely possible with exceptions, but it is not the norm (you can do it yourself, but will a library also do it?) because the norm in Java is to silently pass up exceptions as that is the most ergonomic thing.

And once you start doing it with exceptions, there’s not much difference in the code you end up writing between errors and exceptions.

In practice, I’ve found that when I write Go, I end up annotating most error returns, so the benefit of exceptions for me would be minimal.

replies(1): >>43103586 #
3. Mawr ◴[] No.43101880[source]
If it's easy then why does nobody do it? ;)

In all exception-based languages I know of, catching an exception is so syntactically heavy that annotating intermediate exceptions is never done:

    try {
        Foo()
    } catch (err) {
        throw new Exception("message", err)
    }
One line just turned into four and the call to Foo() is in a nested scope now, ew. At that point even Go is more ergonomic and less verbose:

    err := Foo()
    if err != nil {
        return fmt.Errorf("dfjsdlfkd %w", err)
    }
replies(1): >>43103542 #
4. Capricorn2481 ◴[] No.43103542[source]
> If it's easy then why does nobody do it? ;)

People do this all the time with exceptions.

> One line just turned into four

The Go version has one line of difference?

> At that point even Go is more ergonomic and less verbose

You can't compare it to your Go version because you have to write the error check at every single level, whereas once I throw that exception I can catch it wherever I want. Obviously the Go version will have much more code just around one error.

5. Capricorn2481 ◴[] No.43103586[source]
> And once you start doing it with exceptions, there’s not much difference in the code you end up writing between errors and exceptions

The difference is where you want to catch the error, and not doing a bunch of "plumbing code" for intermediate callers that don't need to know about that error.

> because the norm in Java is to silently pass up exceptions as that is the most ergonomic thing

Adding args to an exception is completely localized. Adding additional args to an error in Go could mean changing dozens of files.

Not to mention I can actually make my own exceptions for the problem. They are like enums with data.

replies(1): >>43103975 #
6. ratorx ◴[] No.43103975{3}[source]
> difference is where you want to catch the error

Catching is pretty similar no? In Java you match by type and in Go, you match by `errors.Is`? I guess the static checking in Java js better, but in terms of code written it is no different.

> additional args to error in Go could mean changing dozens of files

Just to be clear, here we are talking about a function that already returns an exception/error and adding args to it? That is also a local change in Go as well. The call site already handle the interface for error, not sure why changing a field or modifying the error message would make a difference.

Arguably, this type of thing is harder in Java. Adding a new type of exception requires modifying all dependent callers to declare/handle the exception (unless they handle the generic Exception), whereas in Go it is a local only change (except if you need to actually handle the error).

replies(1): >>43108149 #
7. Capricorn2481 ◴[] No.43108149{4}[source]
> Catching is pretty similar no?

In Go, you have to "catch" it at every call level.

> Just to be clear, here we are talking about a function that already returns an exception/error and adding args to it

Yes, but adding an arg doesn't mean modifying the error string, it means adding another piece of data which could be a different type. That's another var, and now every call level has to update to pass along that new var. Unless you change the var from a string to a map, which is a whole different set of headaches.

> Arguably, this type of thing is harder in Java. Adding a new type of exception requires modifying all dependent callers to declare/handle the exception

Only if they need to handle it. If you just want it to bubble up, that function doesn't even need to know about that error or what args it has. That's not the case in Go. Every function has to know about every error that passes through. It's the difference between changing two files and changing 10 files.

replies(1): >>43108327 #
8. ratorx ◴[] No.43108327{5}[source]
> That’s another var

By another var, do you mean another return value? That’s not how it works in Go at all. It is possible to do it that way, but that would not be idiomatic.

You have a single error returned regardless of how many “errors” you have (> 0). If you need to return a new error and it is a custom struct that includes fields, you just implement Error interface on the struct and return it as the single error return. If you need to add new args on the struct, nothing changes other than the error implementation.

Do you want to return 2 errors from the same call site? You have to use something like multierror or a custom struct that includes 2 errors and implement the interface yourself. But the actual thing you return is still a single error.

> unless you want to change the var from string to map

Errors are not strings. It is an interface. If you want to return a string, you implement the interface (although it is much simpler to create a new error with errors.New). If you want to change it to a map later, you implement the interface on the map. It is transparent to the caller, because errors are dynamically dispatched the majority of the time.

> only if they need to handle it

Well, every function needs to declare which exceptions it throws, so you will have to modify every function in the call stack if you don’t want to handle it and it is a new type of Exception.

> That’s not the case in Go

That IS the case in Go. The most common pattern is to return an implementation of the error interface. Nothing changes if the underlying type of error changes except (potentially) the sites that want to handle a specific type of error.