๐Ÿค– ๐Ÿšจ๐Ÿšจ ERROR ๐Ÿšจ๐Ÿšจ ๐Ÿค– ๐Ÿšจ๐Ÿšจ ERROR ๐Ÿšจ๐Ÿšจ ๐Ÿค– This is not a drill we’re going downโ— The story’s ๐Ÿ“– coming to a closeโ— The application ๐Ÿ–ฅ๏ธ is going to crashโ— – Calm down โ—โ— This is no time to panic . Like any Gopher worth their salt, we’ll handle our error gracefully.

What’s an error in Go and why should we handle it gracefully? Let’s start with the first question. An error is a value like any other value (int, float64, bool, string, map, … ). It, however, signifies that the result you’re returned is erroneous or bad โŒ in some way. Many times we see errors paired ๐Ÿ with a good value ((string, error)). This is a real shining โœจ achievement of Go. It’s so easy to return multiple values and it’s so easy to see which functions or methods can give bad results. Therefore you’ll never be guessing if a function you call is going to blow up ๐Ÿ’ฅ ๐Ÿ‘€ in your face ๐Ÿ˜‘

It is up to you to handle that error gracefully ๐Ÿฆข But what does that even mean? Well say there’s a fire ๐Ÿ”ฅ in your kitchen. Would you sit around waiting for the fire alarm ๐Ÿšจ and sprinklers ๐Ÿ’ฆ ๐Ÿ’ฆ to go off? That’s not handling a bad situation with grace ๐Ÿฆข What you’d do instead, would be to find a way to put the fire out ๐Ÿงฏ or call the necessary people to get the job done ๐Ÿš’ That same logic applies in our applications we create. When a function func CanGoWrong( ... ) (string, error) { ... } gives us an error we do not ignore it, we handle it gracefully ๐Ÿฆข

Setup

Let’s make our directory errs and the files we want inside of that directory example_test.go errs.go

mkdir errs
touch errs/example_test.go errs/errs.go

Now let’s open up errs.go and for the very first line we’ll add

package errs

Next for example_test.go for the very first line we’ll add

package errs_test

We can import basics/errs into cmd/main.go and run functions from there with go run cmd/main.go and also to run our example_test.go ๐Ÿ‘ we use go test errs/example_test.go in the commandline.

โš ๏ธ Note The reason we don’t call our package error is because that would lead to massive confusion if we wanted the error interface or our error package. We also can’t call it errors as that’s also used in the standard library as a package and would lead to more confusion.

Create Error

OK, so I’m going to repeat this a lot – errors are just values. And like any good ol' value we can initialize (create) them just like everything else we’ve made up to this point. We can do it manually ๐Ÿ’ช or we can get a little help from the standard library๐Ÿ“š๐Ÿ“š๐Ÿ“š. We’ll do both for practice but let’s start with the easier approach and use what’s in the language.

Coding Time!

https://twitter.com/egonelbre

errs.go

// New returns an error based on the way we want it to be made. Which can
// be done with the standard `errors` package or for more formatting options
// the `fmt` package. If the way is not recognized nil is returned.
func New(way string) error {
	switch way {
	case "fmt":
		return fmt.Errorf("we can use fmt to have formatting verbs: %q", way)
	case "errors":
		return errors.New("an error has occurred")
	default:
		return nil
	}
}

example_test.go

Notice the type shown with our %#v formatting verb is an errors.errorString struct aka another packages unexported type sound familiar? ๐Ÿ˜

func ExampleNew() {
	fmt.Printf(
		"%v\n%#v\n%#v\n%+v\n",
		errs.New("fmt"),
		errs.New("fmt"),
		errs.New("errors"),
		errs.New("๐Ÿคท"),
	)
	// Output:
	// we can use fmt to have formatting verbs: "fmt"
	// &errors.errorString{s:"we can use fmt to have formatting verbs: \"fmt\""}
	// &errors.errorString{s:"an error has occurred"}
	// <nil>
}

Errors Are Just Values

First, if we see ๐Ÿ‘€ what an error actually is in the standard library it won’t be so mystical and magical ๐Ÿง™ to us – here it is or if you don’t want to click away it looks like this

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}

๐Ÿช„ TADA!!!! ๐Ÿ˜ What? Not amazed? It’s nothing special you say? Well of course not! It’s just a value, to be more specific an interface value or we’d actually say an unexported interface. And as we already learned from interfaces , if you satisfy the behavior Error() string you implement that interface. We’ve done this before and we’ll do it again! ๐Ÿ’ช๐Ÿ˜ค

Coding Time!

https://twitter.com/egonelbre

errs.go

// realError is a living, breathing, 100% real error. It is important to
// understand that in Go -- errors are values. If the type implements the error
// interface which has 1 method -- Error() string -- it is an error.
type realError bool

// Error is the only method in the builtin error interface. It returns a
// message of what went wrong.
func (e realError) Error() string {
	return "this is a real error that can be returned if something goes wrong"
}

// Custom shows that implementing the builtin error interface is very easy
// to do and can be used to return custom errors instead of the most common
// unexported `errorString` struct in the `errors` package.
func Custom() error {
	return realError(true)
}

example_test.go

The same way fmt.Println and friends use the String() string method of a type if it has one, an error with the Error() string method is called for us automatically. ๐Ÿ‘Œ Nice touch Go ๐Ÿ‘

func ExampleCustom() {
	if err := errs.Custom(); err != nil {
		fmt.Println(err)
	}
	// Output: this is a real error that can be returned if something goes wrong
}

Errors Are Just Values Part 2: โšกElectricโšก Boogaloo

Errors are just values ๐Ÿคช OK, Yeah I get it, but what does that mean?

It means the only important difference between them and an int is they signal something has gone wrong. That’s because we all agree the word error means something went wrong. Beyond that semantic meaning we give. Everything is the same. You can add methods to your custom errors, you can make any type implement an error, you can have your error implement other interfaces, you can have a struct full of fields that can be used in the Error method, you can even extend the error interface (More on that down below). The power of an error is that its an everyday citizen ๐Ÿšถ just doing its part in the application ๐Ÿ™๏ธ

These are all things we’ve already done in previous lessons, so since we know how to deal with values, we know how to deal with errors.

Coding Time!

https://twitter.com/egonelbre

errs.go

// TooBigError is an exported error that will tell the caller if the number
// input is too big.
type TooBigError int64

func (e TooBigError) Error() string {
	return fmt.Sprintf("number too big: %d", e)
}

// phoneNumberError is an unexported error that informs the caller when a
// bad phone number was passed in.
type phoneNumberError string

func (e phoneNumberError) Error() string {
	// We need to explicitly convert e to a string here or else we'll get
	// NOTE(jay): arg e causes a recursive Error method call.
	return fmt.Sprint("phone number must have 10 digits: ", string(e))
}

// InvalidRuneError is an error that let's the caller know the input rune does
// not work with the function.
type InvalidRuneError rune

func (e InvalidRuneError) Error() string {
	// We need to explicitly convert e to a string here or else we'll get
	// NOTE(jay): arg e causes a recursive Error method call.
	return fmt.Sprintf("input rune is not a valid english letter: %q", string(e))
}

// bearer is a simple interface much like error. It is important to recognize
// errors are values, meaning bearer and error are no different from one
// another. Anything you'd expect a normal value to do, error can to.
type bearer interface {
	Bearer() string
}

// UndeadWarrior is a bearer of a great curse and must travel to distant lands
// in hopes of finally removing it from themself.
type UndeadWarrior struct{}

// Bearer is a method like Error that takes no arguments and returns a string.
func (w UndeadWarrior) Bearer() string {
	return "Rise if you would. For that is our curse."
}

func (w UndeadWarrior) String() string { return w.Bearer() }

// ManyCustoms shows how to deal with many custom errors in a single
// function and shows that errors are just values that are returned by also
// returning a bearer which is very similar in behavior to an error.
func ManyCustoms(n uint32, phoneNo string, ltr rune) (bearer, error) {
	if n > uint32(math.Pow(2, 31)) {
		return nil, TooBigError(n)
	}
	nDigits := 0
	for _, r := range phoneNo {
		if r >= '0' && r <= '9' {
			nDigits++
		}
	}
	if nDigits != 10 {
		return nil, phoneNumberError(phoneNo)
	}
	if !(ltr >= 'A' && ltr <= 'Z' || ltr >= 'a' && ltr <= 'z') {
		return nil, InvalidRuneError(ltr)
	}
	return UndeadWarrior{}, nil
}

example_test.go

Here we drive the point home that we return both bearer and error which are two unexported interfaces with eerily similar methods to each other.

func ExampleManyCustoms() {
	if _, err := errs.ManyCustoms(
		uint32(math.Pow(2, 32)-1), "(555)867-5309", 'A'); err != nil {
		fmt.Println(err)
	}
	if _, err := errs.ManyCustoms(0xff, "(555)67-5309", 'z'); err != nil {
		fmt.Println(err)
	}
	if _, err := errs.ManyCustoms(0b1, "(555)867-5309", '๐Ÿคช'); err != nil {
		fmt.Println(err)
	}
	bearer, err := errs.ManyCustoms(0o7, "(555)867-5309", 'G')
	if err != nil {
		panic(err)
	}
	fmt.Println(bearer)
	// Output:
	// number too big: 4294967295
	// phone number must have 10 digits: (555)67-5309
	// input rune is not a valid english letter: "๐Ÿคช"
	// Rise if you would. For that is our curse.
}

Extend and Customize Errors

Extending โ›“๏ธ errors is exactly as we’ve seen before in interfaces because error is an interface, but we can at least ask why would we extend error? The same reason we want to extend any interfaces functionality. We want more specified behavior. There are plenty of custom error types in the standard library ๐Ÿ“š๐Ÿ“š๐Ÿ“š but one very common one is the net.Error which looks like

type Error interface {
	error
	Timeout() bool   // Is the error a timeout?
	Temporary() bool // Is the error temporary?
}

See how they embed the error interface into their new interface and then add more specific behaviors that you would expect from a HTTP Response. ๐Ÿ—’๏ธโœ๏ธNote if you don’t know what to expect from a HTTP response, don’t worry as we’re about to make our own extended custom error!

Coding Time!

https://twitter.com/egonelbre

errs.go

// ConnectionError extends the behavior of a basic error with more methods that
// could be useful for someone making a call. It can be used to check if
// someone missed the call and try again or if the number had been
// disconnected.
type ConnectionError interface {
	error
	Disconnect() bool // Is the person disconnected?
	Miss() bool       // Did they miss the contact?
}

// CallError implements a ConnectionError. We can imagine other Errors that
// implement ConnectionError like: TransreceiverError, MorseError,
// NetworkError, etc ...
type CallError struct{ Number string }

func (e CallError) Error() string {
	var reason string
	switch {
	case e.Disconnect():
		reason = "it has been disconnected"
	case e.Miss():
		reason = "no one picked up the phone"
	default:
		reason = "something went wrong, please try again"
	}
	return fmt.Sprintln("the number you dialed could not be reached:", reason)
}

// Disconnect satisfies part of ConnectionError.
func (e CallError) Disconnect() bool {
	if e.Number[:3] == "555" {
		return true
	}
	return false
}

// Miss satisfies part of ConnectionError.
func (e CallError) Miss() bool {
	if e.Number[0] == '7' {
		return true
	}
	return false
}

// ExtendBasic shows how to extend the simple error interface to have more
// functionality using composition and embedding the error interface into our
// new ConnectionError.
func ExtendBasic(phoneNo string) ConnectionError {
	return CallError{Number: phoneNo}
}

example_test.go

func ExampleExtendBasic() {
	if err := errs.ExtendBasic("555-212-4958").(errs.ConnectionError); err != nil {
		fmt.Printf("%#v\n%s\n", err, err)
	}
	if err := errs.ExtendBasic("777-390-9911").(errs.ConnectionError); err != nil {
		fmt.Printf("%#v\n%v\n", err, err)
		if err.Miss() {
			fmt.Println("Call again...")
		}
	}
	// Output:
	// errs.CallError{Number:"555-212-4958"}
	// the number you dialed could not be reached: it has been disconnected
	//
	// errs.CallError{Number:"777-390-9911"}
	// the number you dialed could not be reached: no one picked up the phone
	//
	// Call again...
}

Wrap Errors

Almost time to wrap this lesson up, but wait! ๐Ÿคจ We have to talk about wrapping. Not this rapping ๐Ÿง๐ŸŽค ๐Ÿ’ฌ “๐Ÿค‘ ๐ŸŽ๏ธ ๐Ÿ‘š ๐Ÿ‘ฉ”, but this wrapping ๐ŸŽ like a ribbon ๐ŸŽ€ around a present. That’s because in applications it’s fine to have errors and to use them appropriately, but as your application grows, you’ll find that more and more of your functions call smaller functions that can return errors.

So what do we do when we see an error is returned from a function? Ignore it? NO ๐Ÿ™… We handle it gracefully.๐Ÿค” Well doesn’t that error we made have all the information it already needs? Yes! It does and good question! ๐Ÿ˜ But what it doesn’t have is context and in the land of the soft wares, context is king ๐Ÿคด Imagine if you told someone close to you

I got in a car crash ๐Ÿš—๐Ÿ’ฅ๐Ÿš— today…

What do they say?

๐Ÿ—ฏ๏ธ What? Are you OK? Where? Did you do it? Is the other car OK? How bad was it?

Context is king ๐Ÿคด and we want to give a little context to our errors when they may be coming from different spaces ๐Ÿ‘พ and places. You may find yourself calling the same func ReallyCoolFunc( ... ) error { ... } in multiple places in your code base. If you receive that error back, sure you know it came from ReallyCoolFunc but where from the several places you called it? Which place is where that error occurred.

Now how do we accomplish wrapping? With the formatting verb %w. It is a special verb that wraps the error with a surrounding error that we create. This is one of those play with it to realize it things.

Coding Time!

https://twitter.com/egonelbre

errs.go

// WrapOtherErrors shows how to put an error inside of another error. This
// is very helpful when you have many moving parts in your application. We want
// to know **where** the error originated and what places it went along the
// way.
func WrapOtherErrors() error {
	if err := pkgBufioCall(); err != nil {
		return pkgHTTPCall(pkgJSONCall(pkgZipCall(err)))
	}
	return nil
}

func pkgHTTPCall(e error) error {
	return fmt.Errorf("http: Server closed: %w", e)
}

func pkgJSONCall(e error) error {
	return fmt.Errorf("json: syntax error, unexpected ',': %w", e)
}

func pkgZipCall(e error) error {
	return fmt.Errorf("zip: not a valid zip file: %w", e)
}

func pkgBufioCall() error {
	return errors.New("bufio.Scanner: token too long")
}

example_test.go

Here we use another of the standard library package errors functions Unwrap to show what the process was like as we unwrap the error.

func ExampleWrapOtherErrors() {
	if err := errs.WrapOtherErrors(); err != nil {
		fmt.Println("Wrapped error:", err)
		for err != nil {
			err = errors.Unwrap(err)
			fmt.Println("Unwrapping error:", err)
		}
	}
	// Output:
	// Wrapped error: http: Server closed: json: syntax error, unexpected ',': zip: not a valid zip file: bufio.Scanner: token too long
	// Unwrapping error: json: syntax error, unexpected ',': zip: not a valid zip file: bufio.Scanner: token too long
	// Unwrapping error: zip: not a valid zip file: bufio.Scanner: token too long
	// Unwrapping error: bufio.Scanner: token too long
	// Unwrapping error: <nil>
}

Why Always Return Nil? ๐Ÿค”

If we haven’t realized up to this point, when a function works properly and one of it’s return values is an error the ending line of the function is always return nil for the error, why? This has more to do with interfaces and how we check if an error occurred in Go.

When you have a trash bin ๐Ÿชฃ and it’s empty, do you say ๐Ÿ’ฌ My trash bin is nothing? No of course not. That makes no sense, you can say ๐Ÿ’ฌ My trash is empty / is filling up / is full. But you can’t say ๐Ÿ’ฌ My trash is nothing.

Well that same logic applies to an interface. Simply declaring the existence of something other than nothing makes it something. ๐Ÿ˜ตโ€๐Ÿ’ซ Or in much more concrete terms ๐Ÿ˜‚ declaring error makes it an error not nil This is because interfaces have a type and a value associated with them. So when you do var myErr *CustomError even doing var myErr *error you’re making something come into existence. And by definition something is not nothing. Therefore to avoid this confusion and to write idiomatic Go code we’ll return nil

Coding Time!

https://twitter.com/egonelbre

errs.go

// NotNil shows that a nil error value does not equal nil. In other words
// setting an error to nil and returning that error will not give you nil. It
// shows the idiomatic Go way of returning nothing if there is no error.
func NotNil(doItWrong bool) error {
	// var incorrect *CallError = nil ๐Ÿ‘ˆ Same as below, but this is wrong because
	// nil is the zero value, but try this explicit form out if you want.
	var incorrect *CallError
	if doItWrong {
		return incorrect
	}
	return nil
}

example_test.go

Here we can see the type is a pointer to errs.CallError struct and it’s value is nil.

func ExampleNotNil() {
	if err := errs.NotNil(true); err != nil {
		fmt.Printf("YAH GOOFED! %#v", err)
	}

	if err := errs.NotNil(false); err != nil {
		fmt.Println("Never going to see this")
	}
	// Output:
	// YAH GOOFED! (*errs.CallError)(nil)
}

Source File ๐Ÿ“„

The Source File

Test File ๐Ÿ“

The Test File