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

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.


Maybe go just isn’t for you? It really doesn’t need every feature of other languages. The error handling is ideal for me, better than any other language. You are always explicit, with every function call, about “what could happen if this fails?”

Maybe passing it up the stack is the best way to handle it, but also maybe it’s better to handle it somewhere in the middle.

The thing that always happens with exceptions in API projects I’ve worked on, is that exceptions can come from any level of the stack, then by default it skips everything in the middle, and the controller has default handlers for what to do in case of an exception.

If there are exceptions you didn’t know existed because of some library or just complex code with dozens of possible exceptions? They still end up being handled in your controller. You need to know exactly what exceptions could happen at every level of the stack, and how to handle it, otherwise everything just short circuits.

With the go errors, you only need to know “did this function call work? If not, then what?”


Exceptions are a terrible idea.

However, I strongly prefer rust error handling to Go.

go:

   (res, err) := foo()
   if err != nil
       return err
   (res, err) := bar(res)
   if err != …
Equivalent rust:

   let res = bar(foo)?)?;
I think go should add the ? sigil or something equivalently terse.

Ignoring all the extra keystrokes, I write “if err == nil” about 1% of the time, and then spend 30 minutes debugging it. That typo is not possible in idiomatic rust.


I don't want ")?)?;" in Go. I prefer readable syntax rather than symbol soup.

In Rust, there are cases where I have seen at least 8 consecutive symbols. Not a fan of that.


99% of people who never written go will know what the go version does


Then... look it up?

If someone saw "go funcName()" for the first time, would they know how it worked without looking it up?


Prob not but I would still argue go is way easier to read than Rust for someone who does not know either.


and 99% of the people will learn the ? shortcut in fraction second. it's just like every other operator ffs. are you dumbfounded everytime you see the channel operators (->) ? noone takes a second thought to them after the first couple of seconds when they encounter them the first time.


This would drive me nuts to write as a Scala dev, but I can see merit to the philosophy. Go basically lowers the ceiling to raise the floor. Meanwhile Scala can let you glimpse the heavens but has no problem showing you the deepest, darkest pits of hell.


> Exceptions are a terrible idea.

I hear people say this frequently, but don't ever hear people actually state the case.

My experience is that particularly for web back-end exceptions have been incredibly useful, but I'm interested in the counter view.


equivalent exceptions:

    let res = bar(foo());
(I think you meant: let res = bar(foo()?)?;


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.


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.


Exceptions are fine if you never catch them. So is calling abort(). (Which is the Unix way to do what you described.)

If you need to handle errors, you quickly get into extremely complicated control flow that you now have to test:

   // all functions can throw, return nil or not.
   // All require cleanup.
   try {
      a = f();
      b = a.g();
   } catch(e) {
      c = h();
   } finally {
      if a cleanup_a() // can throw 
      if b cleanup_b() // null check doesn’t suffice…
      if c cleanup_c()
   }
Try mapping all the paths through that mess. It’s 6 lines of code. 4 paths can get into the catch block. 8 can get to finally. Finally multiplies that out by some annoying factor.


> So is calling abort(). (Which is the Unix way to do what you described.

But in gui applications and servers, you will need to catch and report the error in some intermediate boundary, not exit the application. That's where go falls short.


bracket pattern

    def bracket[A, T](ctor: () -> A, next: (a: A) -> T): T =
        val a = ctor();
        try { return next(a) } finally { a.dispose() }


Can you give the equivalent in Go?


I recommend ignoring the other reply you just got. They are clearly building a bad faith argument to try to make Go look terrible while claiming to sing its praises. That is not at all how that would look in Go. The point being made was that the exception-based code has lots of hidden gotchas, and being more explicit makes the control flow more obvious.

Something like this:

    a, err := f()
    if err != nil {
        c, err := h()
        if err != nil {
            return fmt.Errorf("h failed: %w", err)
        }
        cleanupC(c)
        return fmt.Errorf("f failed: %w", err)
    }
    defer cleanupA(a)

    b, err := a.g()
    if err != nil {
        c, err := h()
        if err != nil {
            return fmt.Errorf("h failed: %w", err)
        }
        cleanupC(c)
        return fmt.Errorf("a.g failed: %w", err)
    }
    defer cleanupB(b)

    // the rest of the function continues after here
It’s not crazy.

With Java’s checked exceptions, you at least have the compiler helping you to know (most of) what needs to be handled, compared to languages that just expect you to find out what exceptions explode through guess and check… but some would argue that you should only handle the happy path and let the entire handler die when something goes wrong.

I generally prefer the control flow that languages like Rust, Go, and Swift use.

Errors are rarely exceptional. Why should we use exceptions to handle all errors? Most errors are extremely expected, the same as any other value.

I’m somewhat sympathetic to the Erlang/Elixir philosophy of “let it crash”, where you have virtually no error handling in most of your code (from what I understand), but it is a very different set of trade offs.


Or, if you really hate duplication, you could optionally do something like this, where you extract the common error handling into a closure:

    handleError := func(origErr error, context string) error {
        c, err := h()
        if err != nil {
            return fmt.Errorf("%s: h failed: %w", context, err)
        }
        cleanupC(c)
        return fmt.Errorf("%s: %w", context, origErr)
    }

    a, err := f()
    if err != nil {
        return handleError(err, "f failed")
    }
    defer cleanupA(a)

    b, err := a.g()
    if err != nil {
        return handleError(err, "a.g failed")
    }
    defer cleanupB(b)

    // the rest of the function continues after here


[flagged]


This is satire, right?


No.

It is just an explicit rendition of a complex topic.

Do you have a more economical example that handles all of the corner cases of cleanup routines throwing errors?


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.


It's perfectly possible, and a lot less work, to wrap exceptions on their way up the call stack. The difference is you have to remember it at EVERY SINGLE freaking call site in Go.


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


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


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


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.


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


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


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


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


I don't see how this is different from exceptions though? Exceptions just make it optional if you want to handle it at that level (and you can).


Having exceptions means that every line in your function is a potential exit point, often without you being aware of it. This can lead to bugs when a non-atomic operation is abruptly terminated, and you might not realize it just by glancing at the code.

When we were rewriting some code from PHP to Go, I remember that simply thinking about "what to do with err" led me to realize we had a ticking time bomb - one that could explode in production. We had never realized that a certain line of PHP code could potentially throw an exception, and letting it bubble up the stack would have resulted in data corruption. With Go's explicit error handling, this issue became immediately obvious.


It's rather the opposite, it helps avoid bugs when something is terminated, e.g. by ensuring that if an exception is thrown it rolls back a database transaction.

Generally you want to do several classes of things with exceptions in web apps:

1. Return a 500, possibly rendering a nice error page.

2. Log why it occurred.

3. Roll back changes you were making at the time in the database.

And you might also want to do other things, like propagate the error via tracing, increment monitored metrics and many other things. All these are easily done with exceptions, and will often be done by frameworks for you. In desktop apps you may also want to trigger a crash reporter, display a sorry message to the user, and if the exception was inside something like a button click handler you may even be able to proceed safely too.

Given the level of bugginess in untested error handling codepaths that crop up in languages without them, exceptions are definitely the way to go.


Not everything can be wrapped in a single transaction, though. The problem in question involved talking to other services. Frameworks wouldn't help out of the box.


Go can abort at any point as well.

Also, ignoring an error condition (by either forgetting about it, or simply doing the nice and tidy if err dance with no real error handling in place, just a log or whatever) is much worse and can lead to silent data corruption.


You can abort via panic(), but that is expected to crash the application, which is perfectly fine. It's the act of attempting to catch the abort that is fraught with problems. While in Go that is rare, in languages with exceptions it's normal and expected.

Ignoring an error condition is possible in Go, but so unlikely that it's not practical to worry about it.

As an aside, not that it matters, but logging an error is one of the valid ways of handling it, depending on context.


Logging can be a valid handling of error.

I just don't believe that most errors can be handled locally so besides returning an error (bubbling up), not much can be done. Go makes this part of the happy path, so neither can be easily seen/reasoned about anymore.

Exceptions do auto bubbling up, while languages with ADTs have more strictness than go, and often have some syntactic sugar/macro to help with the common case of returning the error (rust's ?).


>I just don't believe that most errors can be handled locally so besides returning an error

Sure, but explicit error handling reminds you that the call may fail, and you may want to handle it in some way (or not - then you bubble it up). With exceptions, it creates the illusion of a simple linear flow.


>ignoring an error condition (by either forgetting about it

We rely on linters to catch that, which is pretty easy to implement (no expensive intra-procedural analysis needed, which is the case with exceptions).

>Go can abort at any point as well.

Panics, unlike errors, are exceptional situations which generally should not be caught (a programming error, like index out of range). They're usually much rarer than errors.


Linters can't know if a case has been properly handled or not. Just because it logs something it may or may not be the proper semantic way to handle that error.


How can you "let the exception bubble up" when you don't know what "the exception" is nor where it's going to be thrown? The agency you imply here does not exist.

All you can do is what you've described - catch all exceptions at the top level and log them. It's a valid strategy so long as you don't mind your service going down at 3am because someone didn't realize that the call to Foo() on line 5593 could in fact throw exception Bar.

Explicit error handling would make it obvious that Foo() can error, allowing the programmer to do whatever's appropriate, say implement a retry loop. Said programmer would then be sound asleep at 3am instead of playing whack-the-exception.


Nothing in languages that use exceptions prevent you for catching the generic exception type and handling it if you can recover/clean up. As I stated, my own rule of thumb (point (d) above) with respect to handling exceptions is "when I can/need to do something". If I can't do anything, then just bubble it up and log.

Case in point is accessing a remote REST endpoint where I might be throttled or the service might be down. I can do something (retry) so I'll write code that probably looks similar to Go with Err:

    var retries = 0;

    do {
      try {
        result = await makeServiceCall();
      } catch {
        if (retries == 3) throw;
        retries++;
        await Task.Delay(retries * 1000);
      }
    } while (retries < 3)
The exception bubbling mechanism just makes it optional so you determine when you want to stop the bubbling.


No, I disagree with you.

If you are using Go, you have no idea what the error you're going to receive is, because the code you call could be calling other code that you have no idea about. You might use existing code that gets modified to call into another module and then you're going to get a whole set of errors that you aren't expecting and won't be able to react to.

What this means is that you have NO way to handle errors except to just error out and bubble up. Because all you can do is look at the error, the best you can do is throw your hands up and say "okay just returning this error." How is this any better than an exception?


Well, my favorite nightmare with exceptions was code that was littered with

throw NoopException()

You tell me what that did.


I guess I won’t be adding new information to what your “experts” said as well, but hey. I love Go’s error handling. Syntactic sugar like in Rust could’ve made things a bit nicer to they eye, but apart from that: being forced to think about each error path leads in my view to better code. Compared to a fat try/catch and fingers crossed that all goes well in it.


Agreed, every time I jump back into Go I'm at first relieved at how nimble it feels; but it never takes long to remember what a pita error handling is or the kludges you need to write to do basic collection transformation pipelines (compared to Java/Streams, C#/LINQ, C++/std etc).


    > or the kludges you need to write to do basic collection transformation pipelines (compared to Java/Streams, C#/LINQ, C++/std etc).
Why hasn't anyone written a good open source for this problem, now that Go has generics?


It seems like the std library has slowly been adding generic iterators methods. Maybe one day?


Don't know, every time I try to do anything beyond trivial using Go generics I run into some kind of issue. They haven't been around that long, it takes time for ideas to mature.


    > anything beyond trivial using Go generics
This is the first complaint that I have heard about Go generics on this board. I believe you. Can you share a specific example? It might spur some interesting discussion.


Haven't written much Go the last year and a half...

Mostly novel limitations that I'm not used to from C++.

Instantiating generic parameter types is one thing I couldn't figure out at some point, but that's pretty much useless in C# and impossible in Java if I remember correctly.

I recognize the frustration from following implementations in both Java and C# though, it takes a while for generics to settle, each implementation has its own quirks.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

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

Search: