Hacker News new | ask | show | jobs
by kitd 950 days ago
Quite apart from the Python discussion, the author captures why I prefer errors as values (a la Go) to exceptions (a la Java), and I have written both styles for many years.

> Regardless of the specific approach, returning errors as values makes us consider all of the places an error could occur. The error scenarios become self-documenting and more thoroughly handled.

This is so true. Most Java exception handling is a try/catch around about 20 lines of code, with superficial logging/rethrowing and no context about exactly what was being done to what when the exception occurred, just a filename/line# and a probably cryptic error message.

In Go, best practice is something like:

    bytes, err := os.ReadFile("myfile.json")
    if err != nil {
        return nil, fmt.Errorf("reading file %s: %v", "myfile.json", err)
    }

    var data map[string]any
    err = json.Unmarshal(bytes, &data)
    if err != nil {
        return fmt.Errorf("unpacking json from file %s: %v", "myfile.json", err)
    }
This gives you precisely targeted errors that tell exactly what you were doing to what. Your future self will thank you when you're desperately trying to work out what went wrong last thing on a Friday.

If you are going to replicate this with exceptions, it would require much more boilerplate, as his example demonstrates, which is ironic given that is the charge levelled at Go.

2 comments

I think it's kinda the opposite actually... with go, you have to go out of the way to generate those errors, while in Python, most of this is automatic.

In particular, the ReadFile example in Python will raise OSError, and those already include filename and error message. So you'd get the same result with 0 extra lines.

For the second example, the json unmarshalling will not auto-add filename, but its easy enough to do using exception chaining:

    try:
        data = json.loads(bytes)
        process_1(data)
        process_2(data)
    except:
        raise Exception(f"Error while parsing file {filename}")
which will give stacktrace like:

    json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "somefile.py", line 4, in somefile
    Exception: Failed while parsing file.json
Note that's even more detailed than go, with significantly less boilerplate (only 3 lines per function) vs 3 lines per call.

(an anecdote: we've had the team which converted the CLI tool from Python to Golang. The first time they ran the tool, it printed a single line:

    invalid character '"' after top-level value
and that's it. It took a lot of debugging before they could figure out what happened. All because they got used to python doing super-rich traceback automatically, with 0 effort from programmers)
This is fine as far as it goes. The problem is that the exception can only report context that it knows about. By contrast, the Go version allows you to include any extra context you want, to make debugging easier. Eg, your error might not just include file reading or Json parsing, but which transaction or customer was involved at the time. You can do with exceptions but you either have to try/catch every statement, or add generic context in one big catch block.

I accept it's largely personal preference, but having used both mechanisms for many years, I find Go best practices for error handling are simple and easy to follow, and results in easily maintainable code, compared to exception handling which doesn't really come with a simple set of best practices, meaning it is often badly put together or added as an afterthought.

maybe there are some best error practices out there, but people don't follow that. As an example, I just went to github.com, got the top-trending go project ("ko"], search for error and arrived at this line in [0]:

    dtodf, err := os.Open(filepath.Join(filepath.Dir(file), "diffid-to-descriptor"))
    if err != nil {
      return nil, fmt.Errorf("opening diffid-to-descriptor: %w", err)
    }
See how they forgot to put filename in the error message? if there is some sort of error with the file, you'll have to resort to strace to find out what the name is... Not to mention that this very function returns json parse errors without context, and the caller "getMeta" calls multiple functions and returns the errors without context as well...

best practices are nice, but fully automatic is even better. The minimum-effort path in Python produced vastly more useful traces than in Go, and unfortunately too many programmers go mininum-effort path.

[0] https://github.com/ko-build/ko/blob/cfc13deeb6417d7e1582f031...

Exceptions are a very useful tool. In your example, the main program logic is buried in error handling which makes it more difficult to read the code and the code becomes more complex, leading to more bugs. In many cases, it's preferable to have a single error handler centralized in one place, out of line of the main logic. This makes the code more readable, reduces complexity and duplication.