Golang errors

From wikinotes

Go discourages the use of exception-style control-flows,
encouraging the use of errors in return-values instead.

Errors

Basics

Go prefers passing error objects as return values to panicking (go's exception-like behaviour).

Errors can be wrapped (often to add info about where found)
Even when wrapped, errors retain their type.

// define an error-type as constant
var ErrDivisionByZero = errors.New("Cannot divide by 0")

func main() {
    if err := doThing(); err != nil {
        // test for specific error
        errors.Is(err, ErrDivisionByZero)

        // wrap an error in an ad-hoc error
        // ('%w' refers to the original error message)
        if err := doThing(); err != nil {
            fmt.Errorf("doThing: %w", err)
        }
    }
}

Full Example

package main

import "errors"
import "fmt"
import "os"

var ErrDivisionByZero = errors.New("Cannot divide by 0")
var ErrOverNineThousand = errors.New("Cannot sum to over 9000")

func Divide(a int, b int) (result int, err error) {
	if b == 0 {
		return 0, ErrDivisionByZero
	}
	return a / b, nil
}

func DivideThenAdd(a int, b int, c int) (result int, err error) {
	res, err := Divide(a, b)
	if err != nil {
		return 0, fmt.Errorf("DivideThenAdd: %w", err)
	}
    res += c
    if res > 9000 {
        return 0, ErrOverNineThousand
    }
	return res, nil
}

func main() {
	var err error

	res, err := DivideThenAdd(10, 0, 1)
	if err != nil {
		if errors.Is(err, ErrDivisionByZero) {
			fmt.Println("Cannot divide by zero!!")
		} else if errors.Is(err, ErrOverNineThousand) {
			fmt.Println("I'd prefer we kept numbers below 9000")
		}
		os.Exit(1)
	}
	fmt.Println(res)
}

Struct Errors

Errors can be any object that expose the Error() string method.
You can make a struct into an error, and pass it's context on to methods where it is called.

// Define an error type
type ErrDivisionByZero struct {
	FirstNum int
	SecondNum int
}

func (e *ErrDivisionByZero) Error() string {
	return "Cannot divide by zero!"
}

When handling these errors, identify/cast them to a variable to retrieve their methods/fields.
Note that you cannot use errors.Is() to check these types of errors.

// cast error-type more explicitly as your struct
// and retrieve it's embedded info
// ('err' itself is a memory-reference, not a value-obj)
if err := doThing(); err != nil {
    var divByZero *ErrDivisionByZero
    var over9000 *ErrOver9000

    switch {
        case errors.As(err, &divByZero): // <-- attempt to cast err to ErrDivisionByZero into var 'divByZero'
            fmt.Printf("%d/%d is a division by zero!\n", divByZero.FirstNum, divByZero.SecondNum)
            fmt.Println(err.Error())
        case errors.As(err, &over9000):
            // ...
        default:
            // ...
    }
    os.Exit(1)
}

Full Example

package main

import "errors"
import "fmt"
import "os"


type ErrDivisionByZero struct {
	FirstNum int
	SecondNum int
}

func (e *ErrDivisionByZero) Error() string {
	return "Cannot divide by zero!"
}

func Divide(a int, b int) (result int, err error) {
	if b == 0 {
		return 0, &ErrDivisionByZero{a, b}
	}
	return a / b, nil
}

func main() {
	var err error

	res, err := Divide(10, 0)
	if err != nil {
		var divByZero *ErrDivisionByZero
		if errors.As(err, &divByZero) {
			fmt.Printf("%d/%d is a division by zero!\n", divByZero.FirstNum, divByZero.SecondNum)
			fmt.Println(err.Error())
		}
		os.Exit(1)
	}
	fmt.Println(res)
}


Panic

panic

A panic is go's repacement for exceptions.
If a panic is not caught, it bubbles to the top of the application, and it exits with an error.

panic("I encountered an error")

recover

You can check the value of a panic (if one has been raised) using recover().

panic("I encountered an error")  // raise a panic
err := recover()                 // returns nil/error-msg-if-present

It is common to handle panics in deferred functions

func main() {
    fmt.Println("hi")
    panic("I just encountered an error")
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("Error: ", err)
            panic(err)                         // <<-- re-raise panic
        }
    }

    fmt.Println("bye")  // <-- never runs
}

Strategies

Catching Deferred Errors

You may want to handle errors from deferred function calls. you can do this with a closure, but you'll need to return an array of errors.

func DoThing() (result string, errs []error) {   // <-- errs !MUST! be declared here for deferred to be caught
    defer func() {
        err = pipe.Close()
        if err != nil {
            errs = append(errs, err)
        }
    }()

    // other logic...

    return "success", errs
}

Panic on Any Error

While working out a concept it may be useful to just let a program error.

func check(e error) {
    if e != nil {
        panic(e)
    }
}

err := doThing()
check(err)

err := doOtherThing()
check(err)