Skip to content

Commit

Permalink
Add wrapping errors (#517)
Browse files Browse the repository at this point in the history
Fixes #334

* Add wrapping errors.

Before there was one page about error handling.

Now there are three.

A simple page (the first part of the existing page).

Then a new page about wrapping errors.

And the third page is the second half of the existing page (custom errors).

* ... addressed pr feedback.

* ./tools/build was run.
  • Loading branch information
guettli committed Mar 21, 2024
1 parent 3dd6791 commit cdb9266
Show file tree
Hide file tree
Showing 15 changed files with 596 additions and 168 deletions.
2 changes: 2 additions & 0 deletions examples.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Interfaces
Struct Embedding
Generics
Errors
Wrapping and Sentinel Errors
Custom Errors
Goroutines
Channels
Channel Buffering
Expand Down
47 changes: 47 additions & 0 deletions examples/custom-errors/custom-errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// It's possible to use custom types as `error`s by
// implementing the `Error()` method on them. Here's a
// variant on the example above that uses a custom type
// to explicitly represent an argument error.

package main

import (
"errors"
"fmt"
)

// It's possible to use custom types as `error`s by
// implementing the `Error()` method on them. Here's a
// variant on the example above that uses a custom type
// to explicitly represent an argument error.
// A custom error type has usualy the suffix "Error".
type argError struct {
arg int
message string
}

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

func f(arg int) (int, error) {
if arg == 42 {
// In this case we use `&argError` syntax to build
// a new struct, supplying values for the two
// fields `arg` and `message`.
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}

func main() {
// If you want to programmatically use the data in
// a custom error, you'll need to get the error as an
// instance of the custom error with errors.As()
_, err := f(42)
var ae *argError
if errors.As(err, &ae) {
fmt.Println(ae.arg)
fmt.Println(ae.message)
}
}
2 changes: 2 additions & 0 deletions examples/custom-errors/custom-errors.hash
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
e7d16589797f94ba3ad1e75449b5f27c71d73a41
9pZvObLbmb8
2 changes: 2 additions & 0 deletions examples/custom-errors/custom-errors.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
42
can't work with it
52 changes: 4 additions & 48 deletions examples/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,72 +16,28 @@ import (

// By convention, errors are the last return value and
// have type `error`, a built-in interface.
func f1(arg int) (int, error) {
func f(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
}

// It's possible to use custom types as `error`s by
// implementing the `Error()` method on them. Here's a
// variant on the example above that uses a custom type
// to explicitly represent an argument error.
type argError struct {
arg int
prob string
}

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

func f2(arg int) (int, error) {
if arg == 42 {

// In this case we use `&argError` syntax to build
// a new struct, supplying values for the two
// fields `arg` and `prob`.
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}

func main() {

// The two loops below test out each of our
// error-returning functions. Note that the use of an
// inline error check on the `if` line is a common
// idiom in Go code.
for _, i := range []int{7, 42} {
if r, e := f1(i); e != nil {
fmt.Println("f1 failed:", e)
if r, e := f(i); e != nil {
fmt.Println("f failed:", e)
} else {
fmt.Println("f1 worked:", r)
fmt.Println("f 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)
}
}

// If you want to programmatically use the data in
// a custom error, you'll need to get the error as an
// instance of the custom error type via type
// assertion.
_, e := f2(42)
if ae, ok := e.(*argError); ok {
fmt.Println(ae.arg)
fmt.Println(ae.prob)
}
}
4 changes: 2 additions & 2 deletions examples/errors/errors.hash
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
00affa44cc98f14c2b10934a4187c9445fac0fe6
NiJOpCPO3L0
bd2a75026d7959adf8e633c6084343740dfe842e
o2xrWWk_1MU
8 changes: 2 additions & 6 deletions examples/errors/errors.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
$ go run errors.go
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
f worked: 10
f failed: can't work with 42
# See this [great post](https://go.dev/blog/error-handling-and-go)
# on the Go blog for more on error handling.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Wrapping errors is a technique that allows you to add additional context
// to an error while preserving the original error.
// This approach is beneficial for debugging and understanding
// the chain of events that led to an error, especially in
// complex applications with multiple layers of function calls.

package main

import (
"errors"
"fmt"
"math/rand/v2"
)

// A sentinel error is a predeclared error variable that is used to
// signify a specific error condition. It allows error values to be compared
// directly via errors.Is() specific types of errors.
var ErrOutOfTea = fmt.Errorf("no more tea available")

var ErrPower = fmt.Errorf("can't boil water")

func MakeTea() error {
if rand.Int32N(4) == 0 {
return ErrOutOfTea
}
if rand.Int32N(7) == 0 {
// You can wrap an error with %w
return fmt.Errorf("add custom text: %w", ErrPower)
}
return nil
}

func main() {
err := makeTeaSeveralTimes()
if err != nil {
fmt.Println("One or serveral errors occured")
}
}

func makeTeaSeveralTimes() error {
var allErrs []error
for range 14 {
err := MakeTea()
if err != nil {
allErrs = append(allErrs, err)
// errors.Is is a function in Go that checks if a given error
// matches a specific error value. It's used to determine whether
// an error (or any error in its chain) is equivalent to a particular
// target error. This is especially useful with wrapped or nested errors,
// allowing you to identify specific error types or sentinel errors in
// a chain of errors.
// By using several if-statements we can handle
// different sentinel errors.
// A switch statement is not applicable here.
if errors.Is(err, ErrOutOfTea) {
fmt.Println("We should buy new tea!")
continue
}
if errors.Is(err, ErrPower) {
fmt.Println("Now it is dark.")
continue
}
fmt.Printf("Some unknown error: %s", err)
continue
}
fmt.Println("The tea is warm and inviting.")
}

// Several errors can be joined to one error.
return errors.Join(allErrs...)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
7e7e1b26a554f1737d79c8f64336548711eae60a
9Ck6s3dPplR
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
$ go run wrapping-and-sentinel-errors.go
The tea is warm and inviting.
The tea is warm and inviting.
The tea is warm and inviting.
The tea is warm and inviting.
We should buy new tea!
The tea is warm and inviting.
The tea is warm and inviting.
The tea is warm and inviting.
Now it is dark.
The tea is warm and inviting.
The tea is warm and inviting.
The tea is warm and inviting.
We should buy new tea!
The tea is warm and inviting.
One or serveral errors occured
Loading

0 comments on commit cdb9266

Please sign in to comment.