This page looks best with JavaScript enabled

Go errors

 ·  🎃 kr0m

Here is the translated text:


In Go, there is no special mechanism to directly link methods to error types. Errors in Go are simply values that satisfy the Error interface, which has a single method, Error() string. Therefore, any type that implements this method can be used as an error in Go.

This article is quite extensive, so I have organized it as follows:


If you are new to the world of Go, I recommend the following previous articles:


Introduction:

The first thing to keep in mind when dealing with errors is that they should never be ignored; they should always be parsed and handled accordingly.

By convention in Go, the error value returned by a function should always be the last one, for example:

func f1(arg int) (int, error) {
    if arg == 42 {
        return -1, errors.New("can't work with 42")
    }
    return arg + 3, nil
}

We can create custom errors very easily; we just need to satisfy Go’s Error interface, which requires a single method, Error() string.

type error interface {
    Error() string
}

When we use fmt.Println() to display an error message, it shows the output of the Error() method of the error object.


Custom error:

The simplest way to create custom errors is by using the New() method from the errors package , where we can see that the method returns an errorString structure with the field s containing the error text and the Error() method associated with that structure, which simply returns the s field of the structure.

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

Let’s see an example:

package main

import (
    "errors"
    "fmt"
)

func f1(arg int) (int, error) {
    if arg == 42 {
        return -1, errors.New("can't work with 42")
    }
    return arg + 3, nil
}

func main() {
    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
}
f1 worked: 10
f1 failed: can't work with 42

Custom error2:

Another way to generate slightly more elaborate errors, where we can introduce variable placeholders, is by using fmt.Errorf().

package main

import (
    "errors"
    "fmt"
)

func f1(arg int) (int, error) {
    if arg == 42 {
        return -1, fmt.Errorf("can't work with: %v", arg)
    }
    return arg + 3, nil
}

func main() {
    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
}
f1 worked: 10
f1 failed: can't work with: 42

Custom error advanced:

Knowing that errors must satisfy the Error() string interface, we can create a structure called argError, for example, with custom fields and link it to the Error() function using Go methods .

Next, in the f2() function, we create an object of type argError, passing it the values expected by the structure.

type argError struct {
    arg  int
    prob string
}

func (a argError) Error() string {
    return fmt.Sprintf("%d - %s", a.arg, a.prob)
}

func f2(arg int) (int, error) {
    if arg == 42 {
        argumentError := argError {
            arg: arg, 
            prob: "can't work with it",
        }
        return -1, argumentError
    }
    return arg + 3, nil
}

Let’s see the complete example:

package main

import (
    "errors"
    "fmt"
)

type argError struct {
    arg  int
    prob string
}

func (a argError) Error() string {
    return fmt.Sprintf("%d - %s", a.arg, a.prob)
}

func f1(arg int) (int, error) {
    if arg == 42 {
        return -1, errors.New("can't work with 42")
    }
    return arg + 3, nil
}

func f2(arg int) (int, error) {
    if arg == 42 {
        argumentError := argError {
            arg: arg, 
            prob: "can't work with it",
        }
        return -1, argumentError
    }
    return arg + 3, nil
}

func main() {
    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
    for _, i := range []int{7, 42} {
        if r, e := f2(i); e != nil {
            fmt.Println("f2 failed:", e)
        } else {
            fmt.Println("f2 worked:", r)
        }
    }
}

Run the code:

f1 worked: 10
f1 failed: can't work with 42
f2 worked: 10
f2 failed: 42 - can't work with it

As we can see, fmt.Println("f2 failed:", e) is displaying the output of the Error() method of the argError type object.

func (a argError) Error() string {
    return fmt.Sprintf("%d - %s", a.arg, a.prob)
}
f2 failed: 42 - can't work with it

Assertion

When a data type satisfies an interface, it loses all fields and methods not required by that interface. Since Go’s Error interface only requires a single method, Error() string, this method is all we can access. But as we explained earlier, we can retrieve these fields using Go’s assertion .

In our case, it would look like this:

    _, e := f2(42)
    if fieldsStructure, ok := e.(argError); ok {
        fmt.Println(fieldsStructure.arg)
        fmt.Println(fieldsStructure.prob)
    }

Let’s see the complete example:

package main

import (
    "errors"
    "fmt"
)

type argError struct {
    arg  int
    prob string
}

func (a argError) Error() string {
    return fmt.Sprintf("%d - %s", a.arg, a.prob)
}

func f1(arg int) (int, error) {
    if arg == 42 {
        return -1, errors.New("can't work with 42")
    }
    return arg + 3, nil
}

func f2(arg int) (int, error) {
    if arg == 42 {
        argumentError := argError {
            arg: arg, 
            prob: "can't work with it",
        }
        return -1, argumentError
    }
    return arg + 3, nil
}

func main() {
    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
    for _, i := range []int{7, 42} {
        if r, e := f2(i); e != nil {
            fmt.Println("f2 failed:", e)
        } else {
            fmt.Println("f2 worked:", r)
        }
    }
    _, e := f2(42)
    if e != nil {
        if fieldsStructure, ok := e.(argError); ok {
            fmt.Println(fieldsStructure.arg)
            fmt.Println(fieldsStructure.prob)
        } else {
            fmt.Println("Error: ", e)
        }
    }
}

Run the code:

f1 worked: 10
f1 failed: can't work with 42
f2 worked: 10
f2 failed: 42 - can't work with it
42
can't work with it

Errors.As:

There is another way to access the fields of the argError object without resorting to assertion, but the receiver parameter of the structure’s method must receive a pointer to the object and not a copy. Functions like f2() that return an error object of this type must return a pointer to the object. Finally, in main(), we need to create a pointer-type argError variable, which we will use as an input parameter in the errors.As() function. If errors.As() successfully converts the error to the specified type, it will return True.

package main

import (
    "errors"
    "fmt"
)

type argError struct {
    arg  int
    prob string
}

//func (a argError) Error() string {
func (a *argError) Error() string {
    return fmt.Sprintf("%d - %s", a.arg, a.prob)
}

func f1(arg int) (int, error) {
    if arg == 42 {
        return -1, errors.New("can't work with 42")
    }
    return arg + 3, nil
}

func f2(arg int) (int, error) {
    if arg == 42 {
        argumentError := argError{
            arg:  arg,
            prob: "can't work with it",
        }
        //return -1, argumentError
        return -1, &argumentError
    }
    return arg + 3, nil
}

func main() {
    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
    for _, i := range []int{7, 42} {
        if r, e := f2(i); e != nil {
            fmt.Println("f2 failed:", e)
        } else {
            fmt.Println("f2 worked:", r)
        }
    }

    _, e := f2(42)
    if e != nil {
        //if fieldsStructure, ok := e.(argError); ok {
        var pErr *argError
        if errors.As(e, &pErr) {
            fmt.Println(pErr.arg)
            fmt.Println(pErr.prob)
        } else {
            fmt.Println("Error: ", e)
        }
    }
}
f1 worked: 10
f1 failed: can't work with 42
f2 worked: 10
f2 failed: 42 - can't work with it
42
can't work with it

Personally, I don’t see any advantage in using assertion directly; in addition, with assertion, there is no need to create pointers.


Panic/Recover functions:

The idiomatic way to handle abnormal conditions in Go is by using errors, which cover the vast majority of possible cases. It is important to remember to use errors and avoid panic/recover as much as possible.

We should only use panic/recover in very specific cases where the program’s execution cannot continue. Some examples might be:

  • A web server that fails to bind to a port; in this case, it is reasonable to use a panic since nothing else can be done.
  • An error in a function that is called internally, where the programmer made a mistake, such as calling the function with incorrect input parameters.

When a panic occurs, the process is as follows:

  • The program’s execution is immediately stopped.
  • Any pending defers in the function where the panic occurred are executed.
  • The function returns without executing the rest of the code.
  • The three previous steps are repeated in the higher-order function.
  • When it reaches the main function, the stack trace is printed.

Let’s look at a simple example where the fullName() function is called with incorrect parameters.

package main

import (
    "fmt"
)

func fullName(firstName *string, lastName *string) {
    defer fmt.Println("Deferred call in fullName")
    if firstName == nil {
        panic("Runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("Runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("Returned normally from fullName")
}

func main() {
    defer fmt.Println("Deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("Returned normally from main")
}
Deferred call in fullName
Deferred call in main
panic: Runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0x32b9f7c76ee0?, 0x32b9f5a005b8?)
	/home/kr0m/test/test.go:13 +0x17a
main.main()
	/home/kr0m/test/test.go:22 +0x85
exit status 2

In the stack trace, we can see that the panic occurred on line 13 in the fullName() function:

main.fullName(0x32b9f7c76ee0?, 0x32b9f5a005b8?)
	/home/kr0m/test/test.go:13 +0x17a

And that it was called from line 22 in the main() function.

main.main()
	/home/kr0m/test/test.go:22 +0x85

The panic/recover combination can be considered similar to the try-catch-finally concept in other programming languages. To recover from a panic, we must execute the recover() function from a defer.

The panic/recover process is as follows:

  • The program’s execution is immediately stopped.
  • Any pending defers in the function where the panic occurred are executed.
  • By executing recover(), the panic situation is cleared.
  • The function returns without executing the rest of the code.
  • Execution continues in the higher-level function as if nothing had happened.
package main

import (
    "fmt"
)

func recoverFullName() {
    if r := recover(); r != nil {
        fmt.Println("Recovered from ", r)
    }
}

func fullName(firstName *string, lastName *string) {
    defer recoverFullName()
    if firstName == nil {
        panic("Runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("Runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("Returned normally from fullName")
}

func main() {
    defer fmt.Println("Deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("Returned normally from main")
}
Recovered from  Runtime error: last name cannot be nil
Returned normally from main
Deferred call in main

We can also obtain the stack trace from a recover().

package main

import (
    "fmt"
    "runtime/debug"
)

func recoverFullName() {
    if r := recover(); r != nil {
        fmt.Println("Recovered from ", r)
        debug.PrintStack()
    }
}

func fullName(firstName *string, lastName *string) {
    defer recoverFullName()
    if firstName == nil {
        panic("Runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("Runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("Returned normally from fullName")
}

func main() {
    defer fmt.Println("Deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("Returned normally from main")
}
Recovered from  Runtime error: last name cannot be nil
goroutine 1 [running]:
runtime/debug.Stack()
	/usr/local/go121/src/runtime/debug/stack.go:24 +0x5e
runtime/debug.PrintStack()
	/usr/local/go121/src/runtime/debug/stack.go:16 +0x13
main.recoverFullName()
	/home/kr0m/test/test.go:11 +0x70
panic({0x483c00?, 0x4b3dc0?})
	/usr/local/go121/src/runtime/panic.go:914 +0x21f
main.fullName(0x1ee600676ee0?, 0x1ee5fe4005b8?)
	/home/kr0m/test/test.go:21 +0x130
main.main()
	/home/kr0m/test/test.go:30 +0x85
Returned normally from main
Deferred call in main

The only limitation of recover() is that it cannot be used from other go routines. Here we can see an example where the divide() function panics when dividing by 0, but since it is running in another go routine, the recover() function, which was deferred in sum(), is not executed.

package main

import (
    "fmt"
)

func recovery() {
    if r := recover(); r != nil {
        fmt.Println("Recovered:", r)
    }
}

func sum(a int, b int) {
    defer recovery()
    fmt.Printf("%d + %d = %d\n", a, b, a+b)
    done := make(chan bool)


    go divide(a, b, done)
    <-done
}

func divide(a int, b int, done chan bool) {
    fmt.Printf("%d / %d = %d", a, b, a/b)
    done <- true
}

func main() {
    sum(5, 0)
    fmt.Println("Normally returned from main")
}
5 + 0 = 5
panic: runtime error: integer divide by zero

goroutine 18 [running]:
main.divide(0x5, 0x0, 0x0?)
	/home/kr0m/test/test.go:22 +0xf6
created by main.sum in goroutine 1
	/home/kr0m/test/test.go:17 +0x14d
exit status 2

If we modify the divide() function to also have a defer recovery(), we will execute the recovery, but as we know, when recovering, the panic state is cleared, but it returns directly to the higher function without executing the rest of the code in the function that caused the panic.

In this case, it returns to sum() without having written to the done channel beforehand, causing a deadlock because sum() is blocked until someone writes to that channel.

package main

import (
    "fmt"
)

func recovery() {
    if r := recover(); r != nil {
        fmt.Println("Recovered:", r)
    }
}

func sum(a int, b int) {
    defer recovery()
    fmt.Printf("%d + %d = %d\n", a, b, a+b)
    done := make(chan bool)
    go divide(a, b, done)
    <-done
}

func divide(a int, b int, done chan bool) {
    defer recovery()
    fmt.Printf("%d / %d = %d", a, b, a/b)
    done <- true
}

func main() {
    sum(5, 0)
    fmt.Println("Normally returned from main")
}
5 + 0 = 5
Recovered: runtime error: integer divide by zero
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.sum(0x5, 0x0)
	/home/kr0m/test/test.go:18 +0x159
main.main()
	/home/kr0m/test/test.go:28 +0x1a
exit status 2
If you liked the article, you can treat me to a RedBull here