1. 17
  1. 11

    Maybe there’s good language reason to avoid stacked exceptions… But whenever I deal with errors in go I just want stacked exceptions. These multiple errors were always more verbose to deal with than exceptions (in Java or Python, say). And these multiple errors libraries mostly just make it harder for me to see where the error actually came from.

    It’s almost as hard to get a useful trace (in real world code) in Go as it is in JavaScript which is pretty strange IMO.

    I worked around this once by always wrapping errors from third-party libraries in an error type that includes the trace, but even that isn’t great (trace goes to the library call, not within the library) and feels weird littering my code like this.

    1. 4

      Despite growing up with js I never got used to exceptions. Go’s error handling is indeed verbose, but both js and Python sometimes hane no hint that an error can even occur! Don’t you find that problematic?

      For me the sweet spot is what Rust does. It’s both explicit and succinct.

      Inko also has an interesting take on errors. It’s exceptions, but you need to be explicit about them.

      I’m wondering why you find it difficult to get a real world trace. Is it because people forget to wrap errors? I know I did when I was doing Go.

      1. 2

        Go’s error handling is indeed verbose

        I’ve never truly understood this argument. In most situations it’s not really more verbose than try/catch? But more so with the wrapping integration into print it feels more natural than most others.

        1. 5

          You can’t be serious. Go’s error handling is incredibly verbose, esp with the style guide that forbids you from squishing it onto a single line. Sometimes it seems like half of my code, by line, consists of

          if err != nil {
              return nil,err
          }
          

          And I know you can combine assignments with “if”, which helps slightly, except when it isn’t allowed because of annoying rules about := vs =, or when it makes your code do the wrong thing because of shadowing.

          Edit: in case it’s not obvious: you only need a try-catch block in contexts where you need to do explicit cleanup or handle the exception. Both are rare in languages with RAII.

          1. 2

            I’ve written a lot of

            let x;
            try {
                x = something_that_can_throw();
            } catch (_) {
                x = default_x();
            }
            

            You can say that’s a poorly designed function, but I don’t always control something_that_can_throw.

            1. 1

              More than that you also want to wrap errors to add information.

              1. 1

                Yeah. JS makes it annoying because you have to either declare x outside of the try/catch or use var. I sometimes use a one line helper that converts a try/catch to [value, error], like let [x, err] = tryTo(() => something_that_can_throw());

              2. 2

                I can be serious and I am. Now you might still not agree and that’s fine, but if you are interested why…

                First of all I don’t think squishing something into one line is less verbose.

                The other thing is that we are talking about handing errors and not just pushing them forward. I also like the distinction between the exception part (which is basically panic, which oftentimes isn’t the thing you want to do) and actually handing an error.

                What you are describing is passing down an error, which is hardly handling it in first place. In most serious software and cases you also want to have a way to wrap it, adding additional information.

                I also like how there is the pattern of using MustX for further distinction and can decide if something is a hard programming error if it fails, which is especially great for creating a distinction for what input is trusted so to speak, for example is the regex defined in the code or is it user input.

                But say you just want to pass it down. Then you have that if statement. It isn’t that much to type and it’s a simple pattern that is very easy to follow, whether you wrap an assignment in there or not. Compared to most scripting languages you are way more in the clear about whether and what errors can be returned.

                Let’s take a more real life example. Let’s say you have a function that makes an HTTP request, that you need, in some code that doesn’t only that one thing, but for example stores the result in a database. If it fails you of course want to be informed what exactly happened and where. Since all of that goes into a log file someone wants to be able to actually read you need to add information.

                In a simple form (oftentimes one might want to be more precise, but we are talking about syntax) Go that would be something like:

                result, err := doStuff()
                if err != nil {
                    return nil, fmt.Errorf("doStuff failed: %w", err)
                }
                

                The JavaScript-equivalent is manifold. In my experience people do this differently, from modifying err.message to using sometimes pretty big libraries.

                If you just wanted to do some exception bail-out and stop the whole thing giving you a stack trace with a message, you’d use panic, but that’s hardly ever anything one would want for debugability reasons. For small scripts and when you want to be like “well I guess some JavaScript HTTP request failed there is nothing we can do anyways” situations it works, but we didn’t even start at error types.

                And again, I really don’t think that if err != nil is that much of a type job (your IDE might even do that for you) or the result is somehow hard to read, if you really just want to pass it on un-wrapped. People rarely complain about function being long compared to func.

                Just using if statements and making clear that errors are just values, making panic/recover a separate thing, and therefor having that pattern of MustX making that separation clear in my opinion is a pretty solid design.

                Of course it’s also a habit, taste and philosophical thing, but my question is. It’s obviously not the only way. What do exactly do you mean by “verbose”? Read? Write? Or do you talk more a the panic style situation? I assume it also makes a difference what you program. But as someone who programs in Go not because, but despite of Google I really like the way error handling works in Go. It feels a lot more flexible and not with the idea of “somehow catching it down the stack, where relevant context might be lost”.

                There’s also more very subjective things, such as people thinking more about errors and what actually is an error where they have to bail out, or want to do something else instead, by still handling things correctly, retrying, etc. But of course you might not be in a situations where developers don’t think about that anyways or where it matters at all.

                There have been proposals similar to what Inko does, and I think they have been rejected.

                When I think verbose I usually think of whole languages and I think of things like Java. compared to that the short if statement followed by what you actually want to do with the error doesn’t sound verbose, but merely different from others.

              3. 4

                Try/catch can be elided if you would just rethrow

                1. 1

                  The problem is that with traditional try/catch there’s no way to tell if f(x) means “I want to rethrow if f throws” or “lol, I forgot that f throws.” I like how Swift and Rust make it explicit.

              4. 2

                I’ll repost my comment from the previous discussion on this a few days ago for why I prefer other approaches over Go’s.

              5. 3

                This this this.

                Go needs a flag to enable stacktrace capture. It would help with production debugging so much. Currently tracking down errors requires extremely good knowledge of the codebase or very strong error handling conventions in the project.

                1. 3

                  I would love an environment variable to enable stack traces. Something like GODEBUG=stacktrace=1.

                  The problem comes from exported error values. For example, the stack trace for net.ErrClosed would be totally useless because it would point to the location where the error was declared https://cs.opensource.google/go/go/+/refs/tags/go1.19.4:src/net/net.go;l=665

                  1. 1

                    Sure, that’s one issue. Error values are a bit of a smell, maybe type checking could be handled within errors.Is() if vet would deprecate checking errors with ==. The other 90% of (dynamically created) errors will be so much more helpful.

                    1. 1

                      I disagree about sentinel error values being a code smell. Regardless, fmt.Errorf could replace the stacktrace if the current stack depth was greater than the error’s attached trace depth. So you’d get the location of the deepest wrapping site instead of the top level var declaration.

                  2. 1

                    They tried to add stack traces to errors.New at the same time they added errors.As. It ended up being dropped because the implementation was too complex IIRC. There are third party packages that add stack traces, but obviously, it’s not the same if you have to opt in.

                2. 3

                  This is a band-aid. What Go really needs is syntactic improvements to make error handling less cumbersome, sort of like Swift’s “try” syntax that looks for an error returned from a callee and automatically returns it from the caller. It wouldn’t have to change the compiled code or ABI at all. But the Go team have always had a religious objection to changing error syntax.

                  1. 4

                    There was a proposal for new syntax (well, a built in function) that almost went through but the community didn’t like it. That does not demonstrate that the Go team has a religious objection to it. The proposal was created by a core member of the Go team, Robert Griesemer.

                    Swift’s try, try! and try? are really cool though.

                    1. 1

                      I think the killer objection to the try proposal was that it screwed up code coverage. How do you tell if try foo() covered its sad path?

                      1. 1

                        I’ve always thought that was a weak objection.