Chapter 9 Errors
How to Handle Errors: The Basics
- In most cases, you should set the other return values to zero values when an error is returned. Exception: sentinel errors.
type error interface {
Error() string
}
Why not use exceptions?
- Exceptions add at least one new code path through the code. This may lead to unexpected crashes when exceptions are not properly handled.
- Making errors returned values forces developers to either handle error conditions or explicitly ignore them with
_.- Errors are often handled inside an
ifstatement, making it distinguishable from business logic.
Use Strings for Simple Errors
- A new error can be created with
errors.New()orfmt.Errorf(). The error string is returned by theError()method.- Error messages should not be capitalized nor should they end with punctuation or a newline.
- These functions return a pointer to an error struct. Therefore, two errors created with the same message are not equal using
==.
Sentinel Errors
- Sentinel errors are error variables declared at the package level. They are meant to signal that processing cannot continue because of a problem with the current state.
- They usually have names starting with
Err. - They are tested using equality (
==), e.g.if err == zip.ErrFormat { ... }.
- They usually have names starting with
- Be sure you need a sentinel error before you define one. They are part of your public API, and they should be self-explanatory without additional information.
Errors Are Values
- When using custom errors, never define a variable to be of the type of your custom error. Either explicitly return
nilor define the variable to be of typeerror.
func GenerateError(flag bool) error {
var err CustomError
if flag {
err = ...
}
return err // a struct will always be returned
}
err := GenerateError(false) // err will never be nil
Wrapping Errors
fmt.Errorf()can be used to wrap errors with the special verb%w. The convention is to write: %wat the end.%wallows the error to be added to the error tree.%vand%scan be used to include the error message without wrapping it.
- The type of the returned error implements the
Unwrap() errormethod to support error wrapping. It returns the wrapped error.- Custom error types need to implement the
Unwrap()method to support wrapping.
- Custom error types need to implement the
errors.Unwrap()calls theUnwrap()method and returns the wrapped error if there is one.- In most cases, use
errors.Is()orerrors.As()instead oferrors.Unwrap().
- In most cases, use
Wrapping Multiple Errors
fmt.Errorf()can merge multiple errors by using multiple%wverbs.errors.Join()can also be used to merge multiple errors into one.- The type of the returned error implements the
Unwrap() []errormethod to support multiple wrapped errors. errors.Unwrap()returnsnilif the error implements the[]errorvariant ofUnwrap(). Therefore, you may need to use type switch to access the wrapped errors.- Use
errors.Is()anderrors.As()instead to examine the error tree.
- Use
switch err := err.(type) {
case interface {Unwrap() error}:
// handle single error
innerErr := err.Unwrap()
case interface {Unwrap() []error}:
// handle multiple errors
innerErrs := err.Unwrap()
default:
// no wrapped errors
}
Is and As
errors.Is()checks whether an error or any error it wraps match a specific sentinel error.- Implement the
Is(target error) boolmethod to override the default behavior (compare with==) if the error is not comparable with==. This method also allows comparisons against errors that aren’t identical instances (custom behavior).
- Implement the
func (re ResourceErr) Is(target error) bool {
if other, ok := target.(ResourceErr); ok {
return re.Code == other.Code || re.Resource == other.Resource
}
}
if errors.Is(err, ResourceErr{Resource: "Database"}) {
// catches all database-related errors
}
errors.As()checks whether an error or any error it wraps match a specific type. The second argument is a pointer to a variable of the target type or a pointer to an interface.Asmethod can be implemented to override the default behavior, but it is rarely needed.
Wrapping Errors with defer
- Put code to wrap errors in a
deferclosure at the beginning of a function. This works well if you wrap every error with the same message.
panic and recover
- As soon as a panic happens, the current function exits immediately, and any
defers attached to the current function start running. When thosedefers complete, thedefers of the caller function run, and so on untilmainis reached. The program exits with a message and a stack trace.- If there is a panic in a goroutine, the
deferchain ends at the function used to launch the goroutine.
- If there is a panic in a goroutine, the
recover()can be called within adeferto check whether a panic happened. If so, it returns the value passed topanic()and continues normal execution.- How it reacts to
panic(nil)depends on Go version: before Go 1.21, it returnsniland continues execution; since 1.21,panic(nil)creates a newPanicNilError. - A program exits if any goroutine panics without being recovered.
- How it reacts to
func div60(i int) {
defer func() {
if v := recover(); v != nil {
fmt.Println(v)
}
}()
fmt.Println(60 / i)
}
This looks like exception handling?
- Reserve panics for fatal situations only. Use
recoverto gracefully handle these situations (e.g. logging and shutting down).recoverdoesn’t make clear what could fail. Idiomatic Go favors explicitly outlining possible failure conditions.
recoveris recommended if you are creating a library for third parties - don’t let panics escape the public API. Convert panics into errors at the API boundary.
Getting a Stack Trace from an Error
- Use
fmt.Printfand%+vverb to print an error with its stack trace if the error supports it.