1. 8

  2. 5

    I send this article to new coworkers all the time when explaining my code review comments. To use go as an example (which has somewhat verbose error handling). Say what you want about the verbosity, but every time I see a go error check, I know the error has been handled and isn’t going to terminate the program in an unexpected way (like a python ValueError or a java unchecked exception or a rust fail!).

    if result, err := func(); err != nil {

    If the error checking doesn’t look like the above code, I have to check twice. Same with resource management. If I see a unlock right after a lock, I have pretty good confidence in the code’s locking.

    defer mu.UnLock()

    Contrast that with the below code which makes it hard to reason about locks.

    ... somewhere else...

    I strongly prefer to see code with visally matched Lock/Unlock or Start/Stop. Whenever reviewing code I think of the song That’s what makes the world go round from Sword in the Stone. “Up and Down, Left and Right, for every to there is a fro”

    1. 13

      I strongly prefer to see code with visally matched Lock/Unlock or Start/Stop.

      For paired calls gaining and releasing a resource, rather than having an explicit pair of calls (with different names!) that I have to visually scan for, my favorite way of handling it is to introduce a scope, or something that looks like a scope, to delineate the lifetime of the resource grab. For example a common idiom in Lisp is:

      (with-lock ...
          code that executes while holding the lock

      Equivalently for with-open-file, etc.

      1. 3

        I like that idiom, and I’ve seen it used to great effect in lisp, python, and even never versions of java. I really like automatic resource management/context managers/whatevers. My only hangup is that I sometimes get a nervous tick due to all the extra indentation. Something like this:

        f1 = open()
        f2 = open()
        shutil.copyfileobj(f1, f2)

        Ends up looking like this:

        with open() as f1:
            with open() as f2:
                shutil.copyfileobj(f1, 2)

        I view go’s defer approach as a really nice compromise visually and mentally.

        f1 = open()
        defer f1.close()
        f2 = open()
        defer f2.close()
        io.Copy(f1, 2)
        1. 1

          Ha, that’s interesting. I personally strongly prefer the rightward march actually…

      2. 1

        What do you ferl about the suggested errorhandling from the Go team?


        1. 1

          I need to write a short blot post because my original comment was getting out of hand. I am a fan of not having errors be special cased in the language. The fact that errors are values is elegant. The helper functions recommended in the referenced post are actually a good idea imo, with one big caveat. That helper type should have just been called Result<T> and it only needed to be written once. Go doesn’t provide some language features that would make dealing with errors as values easier.

          1. 1

            One cannot write Result<T> in Go though, so do you think this solution is good? IMO, it side steps the important thing you mentioned earlier: it’s hard to know if your error handling is correct given this idiom.

            1. 2

              I don’t really like their solution. It just seems unclean. If I’m going to do 3 operations and one of them might fail I want to know about and handle the failure immediately. Call me old fashioned, but if you’re going to change your control flow based on an error, I’d like to see a return/panic()/break. I don’t want an error to toggle a flag in a type that turns all subsequent operations into no-ops.

      3. 4

        In the Ocaml world, a consensus seems to be forming that exceptions should not bleed out of an API. Instead errors should be propagated via an Error type (usually called Result.t) which happens to be a friendly monad. This makes reading code quite nice in that the error is part of the function type and you can use the monad to simulate an exception-like control flow, but the compiler has your back. I have found this to work really great in my personal projects, I cannot speak for it at scale but it’s awesome to add a new error type and have the compiler help you out.

        1. 2

          It definitely scales very well in Haskell, but OCaml has difference performance characteristics and cultural tendencies. I’m never sure whether these actually impede scaling of some kinds of monads.

          1. 1

            Ocaml also has polymorphic variants which makes using an Error monad very nice. Does Haskell have something similar?