Golang errors: Difference between revisions

From wikinotes
No edit summary
 
(30 intermediate revisions by the same user not shown)
Line 1: Line 1:
Go seems to discourage the use of exception-style control-flows,<br>
Go discourages the use of exception-style control-flows,<br>
encouraging the use of errors in return-values instead.
encouraging the use of errors in return-values instead.


= Errors =
= Errors =
<blockquote>
<blockquote>
== Error Objects ==
== Basics ==
<blockquote>
<blockquote>
Go prefers passing error objects as return values to exceptions.<br>
Go prefers passing error objects as return values to panicking (go's exception-like behaviour).
Errors can be any object that expose the <code>Error</code> method.


Errors can be wrapped (often to add info about where found)<br>
Even when wrapped, errors retain their type.
<syntaxhighlight lang="go">
// 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)
        }
    }
}
</syntaxhighlight>
{{ expand
| Full Example
|
<syntaxhighlight lang="go">
<syntaxhighlight lang="go">
require "errors"
package main
require "fmt"
 
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
}


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


// build ad-hoc error
func main() {
fmt.Errorf("User does not exist")
var err error
errors.New("User does not exist")
 
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)
}
</syntaxhighlight>
</syntaxhighlight>
}}
</blockquote><!-- Basics -->
== Struct Errors ==
<blockquote>
Errors can be any object that expose the <code>Error() string</code> method.<br>
You can make a struct into an error, and pass it's context on to methods where it is called.


Functions generally return an error as the last value
<syntaxhighlight lang="go">
<syntaxhighlight lang="go">
func doThing (value int, error) {
// Define an error type
    return nil, fmt.Errorf("User does not exist")
type ErrDivisionByZero struct {
FirstNum int
SecondNum int
}
 
func (e *ErrDivisionByZero) Error() string {
return "Cannot divide by zero!"
}
}
</syntaxhighlight>
</syntaxhighlight>
</blockquote><!-- Error Objects -->


== Error Verification ==
When handling these errors, identify/cast them to a variable to retrieve their methods/fields.<br>
<blockquote>
Note that you cannot use <code>errors.Is()</code> to check these types of errors.
<syntaxhighlight lang="go">
// 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)
}
</syntaxhighlight>
 
{{ expand
| Full Example
|
<syntaxhighlight lang="go">
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)
}
</syntaxhighlight>


</blockquote><!-- Error Verification -->
}}
</blockquote><!-- Struct Errors -->
</blockquote><!-- Errors -->
</blockquote><!-- Errors -->


Line 75: Line 204:
</blockquote><!-- panic and recover -->
</blockquote><!-- panic and recover -->
</blockquote><!-- Panic -->
</blockquote><!-- Panic -->
= Strategies =
<blockquote>
== Catching Deferred Errors ==
<blockquote>
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.
<syntaxhighlight lang="go">
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
}
</syntaxhighlight>
</blockquote><!-- Catching Deferred Errors -->
== Panic on Any Error ==
<blockquote>
While working out a concept it may be useful to just let a program error.
<syntaxhighlight lang="go">
func check(e error) {
    if e != nil {
        panic(e)
    }
}
err := doThing()
check(err)
err := doOtherThing()
check(err)
</syntaxhighlight>
</blockquote><!-- Panic on Any Error -->
</blockquote><!-- Strategies -->

Latest revision as of 03:23, 17 July 2022

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)