Go 2 Draft: Error Handling

Dean Bassett on August 29, 2018

Go 2 is being drafted. If you haven't heard, there will be a few major language changes coming to Go 2. The Go team has recently submitt... [Read Full]
markdown guide
 

Eventually you do something like this:

package main

import (
    "log"
    "io/ioutil"
    "os"
)

func main() {
    handle := func (v interface{}, err error) interface{} {
        if err != nil {
            log.Fatal(err)
        }
        return v
    }

    hex := handle(ioutil.ReadAll(os.Stdin))
    data := handle(parseHexdump(string(hex)))
    os.Stdout.Write(data)
}

Which I've seen in some other examples as:

package main

import (
    "log"
    "io/ioutil"
    "os"
)

func main() {
    checkError := func (err error) {
        if err != nil {
            log.Fatal(err)
        }
    }

    hex, err := ioutil.ReadAll(os.Stdin)
    checkError(err)
    data, err = parseHexdump(string(hex))
    checkError(err)
    os.Stdout.Write(data)
}

To be honest, while this proposed solution works, it still looks like we only saving a few keystrokes. It's good but not great.

 

It's totally different. By your way, you can stop the subsequent operations when an error occurs

 

That doesn't work for returning errors, though. What if I wanted to return a wrapped version of the error?

 

Fair enough, but in that case, the wrapping is implicit and it looks like a twisted version of a try/catch block, something that spreads confusion.

I would prefer something like this:

func main() {
    check {
        hex := ioutil.ReadAll(os.Stdin)
        data := parseHexdump(string(hex))
        os.Stdout.Write(data)
    } handle err {
        switch err.(type) {
    case *os.ErrNotExist:
        // Code...
    case ...:
        // ...
    default:
        // ...
    }
}

As its more structured, easier to read and less confusing than the draft proposal. Note that the handle could be triggered multiple times just as the original handle version.

But now there's very implicit error checking, you don't know which lines may result in an error, which is something the proposal was trying to avoid.

 

Although I’m not a go developer I love to check out new languages and concepts they bring. I never really liked the cluttering aspect of error handling in go but this check handle pattern is really confusing.
In the examples from another comment, there were multiple handle blocks. Which one is going to be used? How can I handle two different errors in the same scope differently. Why is the handle block before the check block?

 

To answer your questions -

  1. The handle blocks chain. Once an handle block is reached, it becomes part of the chain. If an error occurs, the blocks execute from the bottommost block to the topmost. (It is proposed to remove the chaining functionality, though)

  2. If you simply want to add functionality, just add another handle block when you want that functionality to start being used. (Like the file-copy example I have in the post). Otherwise you'll have to do the standard if err != nil

  3. There's a few reasons behind this:

a. It's easier to think about needing to "declare" your handle blocks. You can't use a handle block if it hasn't been declared yet!

b. It makes for much faster compile times to require handle blocks.

c. It makes it more similar to how defer works (if you don't know Go this doesn't mean much)

d. Enabling a handle block to be after a check block means that the handle block may have more variables in it's scope than what was at the check block...


// Demonstrates why `handle` must be above `check`
func ErrorTesting() {
    check errors.New("some kind of error") // will go to handle block

    i := 5
    handle err {
        log.Fatalf("i=%d, err=%v", i, err)
        // Since we got here from the top line, i has not been declared!
    }
}
 

Thanks for your elaborate reply.

Maybe I shouldn’t do this, but in my head I compare this to the try/catch/finally concept. Catch comes after try but cannot use any variables that have not been declared before try. But it makes the flow much clearer since you usually read code from top to bottom.
And one of my questions was how to implement multiple catch blocks. With try/catch i can have muktiple of those constructs or even multiple catch blocks. Let’s say I have file operation and a parsing operation and I want to handle them differently, I could declare two different catch blocks. How would I do this with check/handle?

It would look something like this:

In current Go with the handle construct would look something like this:

handle err {

    if notExist, ok := err.(*os.ErrNotExist); ok {
        // Do stuff with notExist
    } else if otherErr, ok := err.(*pack.OtherErr); ok {
        // Do stuff with otherErr
    }
}

// Or if your list is very long
handle err {
    switch err.(type) {
    case *os.ErrNotExist:
        // Code...
    case ...:
        // ...
    default:
        // ...
    }
}

Note that the notExist, ok := err.(*os.ErrNotExist) is how you typecast in Go. Also, a semicolon in an if statement means "just execute the left side normally and evaluate the right side as the condition", although any variables declared on the left side have block scope within the if statement.

Errors in Go are just an interface, so you just cast them to the error that you want to check.

 

Hi Dean,

I'm a little conflicted on this.

First: instead of having the pattern var, err := method() now the code will be "littered" with var := check method(). I hope they can find a way to make that check implicit. I'm still not sure how I feel about those handle blocks, I've only skimmed the draft yesterday.

Second: I like this reaction about the whole thing:

Third: about handle blocks: think about reading the code after you wrote it, maybe weeks after. You read the code, and instead of seeing how the error is handled right there you need to jump up (hopefully not many levels) to see the various conditions, then you can resume reading the code, then you see another check so you go back up with your eyes to see what happens again with the error, then you resume, then you go back up again...

ps. you can add syntax highlighting to the code blocks, like:

// From the draft
func main() {
    handle err {
        log.Fatal(err)
    }

    hex := check ioutil.ReadAll(os.Stdin)
    data := check parseHexdump(string(hex))
    os.Stdout.Write(data)
}

this way:

go with syntax highlighting

 

Implicit error checking is just kinda... bad. As a quote from the draft -

The subtleties of implicit checking are covered well in Raymond Chen’s pair of blog posts, “Cleaner, more elegant, and wrong” (2004), and “Cleaner, more elegant, and harder to recognize” (2005). In essence, because you can’t see implicit checks at all, it is very hard to verify by inspection that the error handling code correctly recovers from the state of the program at the time the check fails.

In terms of that reaction, I actually like that. Once a handle leaves scope, it should no longer be used.
I also think that defer should work the same way. A defer should execute when it leaves its scope, not when the function is done executing. It just makes more sense in my eyes and is much more intuitive.

For the syntax highlighting, I used a capital "G" on accident, I'll fix that!

 

Implicit error checking is just kinda... bad. As a quote from the draft

I meant, implicit usage of check (you can derive which functions need a check if one of the return values is error) but still with explicit error handling.

But now, while writing this, I think I understood. If they remove the explicit check there's no way for the programmer to instantly figure out which method calls can trigger an error, unless they inspect the source code.

Using check also allows the compiler to throw errors if someone forgot to add check.

 

Hi Dean,
I feel like adding handle fallbacks is limiting my chance to spot which place exactly generated the error.
Originally we could generate different errors for various reason. The example above is actually using same messages all the time. I know we have the line number, but I still preferred the "copyfile os.open error %s" format to specify exact spot individually.
I am not saying this won't allow it, it will just look very similar.

I would love it we would have also name of function passed to handler, ie:

handle err, fn {
  log.Fatal(fmt.Errorf("%s %s", fn, err))
}

hex := check ioutil.ReadAll(os.Stdin)
data := check parseHexdump(string(hex))

What do you think ?

 

I actually quite like this idea! I'd suggest heading over to the wiki and put it there!

 

tldr: go maintainers stubbornly refuse to admit that nil and not having union types are their two biggest mistakes.

Mistakes that are unfixable at this point.

 

From the Go 2017 user survey:
2017 user survey

Union/Sum types are never mentioned (although "types" is)... Honestly I think I can live fine without sum types

Error handling, dependency management, and generics are by far the most mentioned things, so those are what they are focusing on.

 

Honestly I think I can live fine without sum types

Honestly I think you have never used them before, aren't you?

Uhhh nope, can't say I've ever had the desire to use them though

I know some people really adore them, and I know they can be useful, but in Go we've got interfaces and maybe even contract-based generics soon, which honestly kinda makes it so we don't really have a large need for union types 🤷‍♂️

Uhhh nope

You doesn't talk like a coder. Real coder will answer "Nope, I used them" or "Indeed, I didn't use them". I don't understand you.

but in Go we've got interfaces

Are you sure about "uhhh nope"? Visitor pattern destroys readability (and performance for short unions replacement - and we actually need only short unions), other interface based approaches in Go don't provide type safety.

even contract-based generics soon

generics are orthogonal to to algebraic/variadic data types. They become much better with generics though and can provide full type safety for error handling (unlike current approach).

I am afraid you are actually have a little sense about the idea.

Don't waste your time arguing. Go is designed to minimize amount of computer science needed to do work. It is NOT designed for productivity or engineer happiness. It is specifically designed to make developers easily replaceable.

 
 

They've said Go 2 will probably be around Go 1.15 or so. So there's still a long time, Go 2 is still in very early planning phase, and they take their time.

code of conduct - report abuse