Errors as values
| Return errors | |
| Check them immediately | |
| Wrap with %w | |
| Custom error types |
Return errors
Go has no exceptions: a function that can fail returns its result together with an error value. Success carries nil, and failure carries a value explaining what went wrong.
package main
import (
"errors"
"fmt"
)
func divide(a float64, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
fmt.Println(divide(7, 2))
fmt.Println(divide(7, 0))
}
go run main.go
3.5 <nil>
0 division by zero
Errors are ordinary values, so they travel through variables, struct fields, and return statements like any other data. The errors.New helper builds a simple error from a message string.
Check them immediately
Handle the error before touching the result. The standard shape is one if statement right after the call: on failure report the problem or return early, otherwise continue with the value.
package main
import (
"errors"
"fmt"
)
func divide(a float64, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(7, 0)
if err != nil {
fmt.Println("oops:", err)
} else {
fmt.Println(result)
}
}
oops: division by zero
Linters flag unchecked errors, and reviewers will too: every function that returns an error expects the caller to look at it. Returning the error upward is fine when the current level has nothing useful to add.
Wrap with %w
Add context while keeping the original error reachable by wrapping it with fmt.Errorf and the %w verb. Callers can then match the root cause with errors.Is even through several layers of context.
package main
import (
"errors"
"fmt"
)
var errEmpty = errors.New("empty name")
func greet(name string) error {
if name == "" {
return fmt.Errorf("greet failed: %w", errEmpty)
}
fmt.Println("Hello,", name)
return nil
}
func main() {
err := greet("")
fmt.Println(err)
fmt.Println(errors.Is(err, errEmpty))
}
greet failed: empty name
true
Use %w for the cause and %v or %s for plain details: only %w links the chain for errors.Is and errors.As. One wrap per level is enough, at the point where fresh context appears.
Custom error types
Anything that implements the error interface with an Error method returning a string counts as an error. Custom types let failures carry structured data such as status codes instead of plain text.
package main
import "fmt"
type HTTPError struct {
Code int
Msg string
}
func (e HTTPError) Error() string {
return fmt.Sprintf("http %d: %s", e.Code, e.Msg)
}
func fetch(code int) error {
if code != 200 {
return HTTPError{Code: code, Msg: "bad response"}
}
return nil
}
func main() {
fmt.Println(fetch(200))
fmt.Println(fetch(404))
}
<nil>
http 404: bad response
Keep custom errors in the package that produces them so callers can match them with errors.As and read their fields. Start with errors.New and grow into custom types only when callers need more than a message.
Next: Pointers
Article author: Arthur Isaev