Golang errors
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 useerrors.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-presentIt 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)