09. Error Handling

Errors

Go treats an error as a value. Such errors do not have the potential to break the programs, but they are designed to denote the anomaly in the business logic

In Go it’s idiomatic to communicate errors via an explicit, separate return value. This contrasts with the exceptions used in languages like Java and Ruby and the overloaded single result / error value sometimes used in C.

Go’s approach makes it easy to see which functions return errors and to handle them using the same language constructs employed for any other, non-error tasks.

Returning an error

By convention, errors are the last return value and have type error, a built-in interface:

func f1(arg int) (int, error) {
    if arg == 42 {
        // `errors.New` constructs a basic `error` value
        // with the given error message.
        return -1, errors.New("can't work with 42")
    }

    // A nil value in the error position indicates that
    // there was no error.
    return arg + 3, nil
}

errors.New constructs a basic error value with the given error message.

Receiving an error

Functions often return an error value, and calling code should handle errors by testing whether the error equals nil. A nil error denotes success; a non-nil error denotes failure.

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

Wrapping an error

When an error is passed back through your code, you often want to add additional context to it. This context can be the name of the function that received the error or the operation it was trying to perform. When you preserve an error while adding additional information, it is called wrapping the error. When you have a series of wrapped errors, it is called an error chain.

The fmt.Errorf function has a special verb, %w. Use this to create an error whose formatted string includes the formatted string of another error and which contains the original error as well. If you want to create a new error that contains the message from another error, but don’t want to wrap it, use fmt.Errorf to create an error, but use the %v verb instead of %w:

The standard library also provides a function for unwrapping errors, the Unwrap function in the errors package. You pass it an error and it returns the wrapped error, if there is one.

func fileChecker(name string) error { f, err := os.Open(name) iferr!=nil{
    return fmt.Errorf("in fileChecker: %w", err) }
    f.Close()
    return nil
}

func main() {
    err := fileChecker("not_here.txt") 
    if err != nil{
       fmt.Println(err)
       if wrappedErr := errors.Unwrap(err); wrappedErr != nil {
            fmt.Println(wrappedErr)
       }
    } 
}

You don’t usually call errors.Unwrap directly. Instead, you use errors.Is and errors.As to find a specific wrapped error.

If you want to wrap an error with your custom error type, your error type needs to implement the method Unwrap.

To check if the returned error or any errors that it wraps match a specific sentinel error instance, use errors.Is. It takes in two parameters, the error that is being checked and the instance you are comparing against. The errors.Is function returns true if there is an error in the error chain that matches the provided sentinel error. By default, errors.Is uses == to compare each wrapped error with the specified error. If this does not work for an error type that you define (for example, if your error is a noncomparable type), implement the Is method on your error:

func main() {
    err := fileChecker("not_here.txt") 
    if err != nil {
        if errors.Is(err, os.ErrNotExist) { 
            fmt.Println("That file doesn't exist")
        } 
    }
}

The errors.As function allows you to check if a returned error (or any error it wraps) matches a specific type. It takes in two parameters. The first is the error being examined and the second is a pointer to a variable of the type that you are looking for.

err := AFunctionThatReturnsAnError() 
var myErr MyErr
if errors.As(err, &myErr) {
    fmt.Println(myErr.Code)
}

Error type

Go programs express error state with error values. The error type is a built-in interface:

type error interface {
    Error() string
}

Example:

type MyError struct {
    When time.Time
    What string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("at %v, %s", e.When, e.What)
}

func run() error {
    return &MyError{
        time.Now(),
        "it didn't work",
    }
}

func main() {
    if err := run(); err != nil {
        fmt.Println(err)
    }
}

errors.New

errors.New function takes a string that it converts to an errors.errorString and returns as an error value:

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

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

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

Sentinel errors

Sentinel errors is there are specific error created as values to indicate specific cases. They are one of the few variables that are declared at the package level. By convention, their names start with Err. They should be treated as read-only; there’s no way for the Go compiler to enforce this, but it is a programming error to change their value.

func main() {
    data := []byte("This is not a zip file")
    notAZipFile := bytes.NewReader(data)
    _, err := zip.NewReader(notAZipFile, int64(len(data))) if err == zip.ErrFormat {
        fmt.Println("Told you so")
    }
}

Sentinel errors are usually used to indicate that you cannot start or continue processing. Be sure you need a sentinel error before you define one. Once you define one, it is part of your public API and you have committed to it being available in all future backward-compatible releases.

Handling panic and recover

A panic is similar to an exception which can occur at runtime. A panic can be thrown by the runtime or can be deliberately thrown by the programmer to handle the worst case scenario. For example, accessing the out of bounds element from a slice (or an array) will throw a panic.

Since the only functions which will get executed when the panic occurs are deferred function, we need to use recover function inside defer function.

The recover function returns the value passed to panic function and has no side effects. That means if our goroutine is not panicking, recover function will return nil. So checking the return value of recover() to nil is a good way to know if your program is packing, unless some idiot passes nil to the panic function which is a very rare case.

  • func panic(interface{})

  • func recover() interface{}

When the Go program panics, Go doesn’t just start unwinding the deferred function stack all at once, that’s not how deferred functions work. If a panic occurs in a function, its deferred function will be called and if program recovers inside those deferred functions, normal execution flow begins after that function exits.

No statements after the function call statement, which panicked, will be executed.

Когда случается panic, текущий фрейм функции уничтожается. Можно обработать painc в defer-функции, но фрейм это не вернет. Выполнение продолжится в вызвавшей функции, но если функция в которой слилась panic должна была возвращать значение, то по-видимому это значение будет повреждено -- будет возвращено пустое значение для своего типа. Проблему можно поправить путем named return value подменив значение на нормальное в defer.

Так же named return values позволяют нормально вернуть значение в случае panic.

Итого, или используй named return values или обрабатывай panic выше по стеку. Будь аккуратен с поврежденным return/

Использование:

  • Функция panic(arg) приостанавливает выполнение текущей функции F.

  • Все defer функции от F выполняются, а так же все связанные с ними прямые и косвенные вызовы других функций.

  • После выполнения defer функций начинается раскрутка стека.

  • Раскрутка стека продолжается пока panic не будет перехвачен вызовом recover()

  • Возвращаемым значением из recover() будет значение arg переданное в panic().

// testPanic simulates a function that encounters a panic to
// test our catchPanic function.
func testPanic(){
    var ok bool
    // Schedule the catchPanic function to be called when
    // the testPanic function returns.
    defer func() {
        // Check if a panic occurred.
        if r := recover(); r != nil {
            if err, ok = r.(error); ok {
                fmt.Println(err)
            }
            fmt.Println("PANIC Deferred")
        }
    }()


    fmt.Println("Start Test")
    panic(fmt.Errorf("At the disco"))

    fmt.Println("End Test")
}

// Start Test
// At the disco
// PANIC Deferred

In order to recover a panicking program successfully, a deferred function with recover call must be executed right after the panic. Because the recover function does not know if the program panicked because from where it was called, everything was OK.

When a panic is detected in a goroutine, we can create a new goroutine for it. An example:

package main

import "log"
import "time"

func shouldNotExit() {
	for {
		// Simulate a workload.
		time.Sleep(time.Second)

		// Simulate an unexpected panic.
		if time.Now().UnixNano() & 0x3 == 0 {
			panic("unexpected situation")
		}
	}
}

func NeverExit(name string, f func()) {
	defer func() {
		if v := recover(); v != nil {
			// A panic is detected.
			log.Println(name, "is crashed. Restart it now.")
			go NeverExit(name, f) // restart
		}
	}()
	f()
}

func main() {
	log.SetFlags(0)
	go NeverExit("job#A", shouldNotExit)
	go NeverExit("job#B", shouldNotExit)
	select{} // block here for ever
}

At any give time, a function call may associate with at most one unrecovered panic. If a call is associating with an unrecovered panic, then

  • the call will associate with no panics when the unrecovered panic is recovered.

  • when a new panic occurs in the function call, the new one will replace the old one to be the associating unrecovered panic of the function call.

For example, in the following program, the recovered panic is panic 3, which is the last panic associating with the main function call.

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println(recover()) // 3
	}()
	
	defer panic(3) // will replace panic 2
	defer panic(2) // will replace panic 1
	defer panic(1) // will replace panic 0
	
	panic(0)
}

runtime.Goexit

When the runtime.Goexit function is called in a function call, we say a Goexit signal starts associating with the function call after the the runtime.Goexit call fully exits. A panic and a Goexit signal are independent of each other. Associating either a panic or a Goexit signal with a function call will make the function call enter its exiting phase immediately.

As Goexit signals can't be cancelled, arguing whether a function call may associate with at most one or more than one Goexit signal is unnecessary.

Capture stack trace

// Capture the stack trace.
buf := make([]byte, 10000)
runtime.Stack(buf, false)
fmt.Println("Stack Trace:", string(buf))

Last updated