Skip to content

Commit

Permalink
Rework errors concepts and exercise (#2666)
Browse files Browse the repository at this point in the history
  • Loading branch information
junedev committed Jun 26, 2023
1 parent e56a1af commit 373215e
Show file tree
Hide file tree
Showing 17 changed files with 592 additions and 540 deletions.
5 changes: 0 additions & 5 deletions concepts/error-wrapping/.meta/config.json

This file was deleted.

3 changes: 0 additions & 3 deletions concepts/error-wrapping/about.md

This file was deleted.

23 changes: 0 additions & 23 deletions concepts/error-wrapping/introduction.md

This file was deleted.

1 change: 0 additions & 1 deletion concepts/error-wrapping/links.json

This file was deleted.

4 changes: 2 additions & 2 deletions concepts/errors/.meta/config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"blurb": "Error handling is NOT done via exceptions in Go. Instead, errors are normal values that are returned as the last return value of a function.",
"authors": ["micuffaro", "brugnara"],
"contributors": ["jmrunkle", "junedev"]
"authors": ["micuffaro", "brugnara", "junedev"],
"contributors": ["jmrunkle"]
}
165 changes: 85 additions & 80 deletions concepts/errors/about.md
Original file line number Diff line number Diff line change
@@ -1,109 +1,101 @@
# About

Error handling is **not** done via exceptions in Go.
Instead, errors are normal values of the interface type `error`.

## Creating and Returning Errors
## The error interface

You do not have to always implement the error interface yourself.
To create a simple error, you can use the `errors.New()` function that is part of the standard library package `errors`.
These error variables are also called _sentinel errors_ and by convention their names should start with `Err` or `err` (depending on whether they are exported or not).
You should use error variables instead of directly writing `errors.New` in cases where you use an error multiple times or where you want the consumer of your package to be able to check for the specific error.
Error handling is **not** done via exceptions in Go.
Instead, errors are normal _values_ of types that implement the built-in `error` interface.
The `error` interface is very minimal.
It contains only one method `Error()` that returns the error message as a string.

```go
import "errors"

var ErrSomethingWrong = errors.New("something went wrong")
ErrSomethingWrong.Error() // returns "something went wrong"
type error interface {
Error() string
}
```

An error is by convention the last value returned in a function with multiple return values.
If the function returns an error, it should always return the zero value for other returned values:
Every time you define a function in which an error could happen during the execution that needs to reach the caller, you need to include `error` as one of the return types.
If the function has multiple return values, by convention `error` is always the last one.

```go
import "errors"

// Do this:
func GoodFoo() (int, error) {
return 0, errors.New("Error")
}

// Not this:
func BadFoo() (int, error) {
return 10, errors.New("Error")
func DoSomething() (int, error) {
// ...
}
```

Return `nil` for the error when there are no errors:
## Creating and returning a simple error

You do not have to always implement the error interface yourself.
To create a simple error, you can use the `errors.New()` function that is part of the standard library package `errors`.
The only thing you need to pass in is the error message as a string, and `errors.New()` will take care of creating a value that contains your message and implements the `error` interface.

If the function returns an error, it is good practice to return the zero value for all other return parameters:

```go
func Foo() (int, error) {
return 10, nil
func DoSomething() (SomeStruct, int, error) {
// ...
return SomeStruct{}, 0, errors.New("failed to calculate result")
}
```

## Custom Error Types
~~~~exercism/caution
You should not assume that all functions return zero values for other return values if an error is present.
It is best practice to assume that it is not safe to use any of the other return values if an error is returned.
If you want your error to include more information than just the error message string, you can create a custom error type.
As mentioned before, everything that implements the `error` interface (i.e. has an `Error() string` method) can serve as an error in Go.
As an example, imagine a function was trying to read from a file and returns a half-filled byte slice and an error.
If you would re-use that returned byte slice in your code, assuming it is always empty because there was error, you can run into subtle bugs.
Usually, a struct is used to create a custom error type.
By convention, custom error type names should end with `Error`.
Also, it is best to set up the `Error() string` method with a pointer receiver, see this [Stackoverflow comment][stackoverflow-errors] to learn about the reasoning.
Note that this means you need to return a pointer to your custom error otherwise it will not count as `error` because the non-pointer value does not provide the `Error() string` method.
The only exceptions are cases where the function documentation clearly states that other returns values are meaningful in case of an error.
Look at the [documentation for `Write` on a buffer][buffer-write] for an example.
~~~~

Make sure the error message you provide is as specific as possible as errors do not include any stack traces by default.
By convention, the error message should start with a lowercase letter and not end with a period.

If you want to use such an error in multiple places (or you want to make the error available to the consumer of your package), you should declare a variable for the error instead of using `errors.New` in-line.
By convention, the name of the variable should start with `Err` or `err` (depending on whether it is exported or not). These error variables are often called _sentinel errors_.

```go
type MyCustomError struct {
message string
details string
}
import "errors"

func (e *MyCustomError) Error() string {
return fmt.Sprintf("%s, Details: %s", e.message, e.details)
}
var ErrNotFound = errors.New("resource was not found")

func someFunction() error {
func DoSomething() error {
// ...
return &MyCustomError{
message: "...",
details: "...",
}
return ErrNotFound
}
```

## Checking for Errors

Errors can be checked against `nil`:
Return `nil` for the error to signal that there were no errors during the function execution:

```go
func myFunc() error {
file, err := os.Open("./users.csv")
if err != nil {
// handle the error
return err // or e.g. log it and continue
}
// do something with file
func Foo() (int, error) {
return 10, nil
}
```

Since most functions in Go include an error as one of the return values, you will see/use this `if err != nil` pattern all over the place in Go code.
## Including variables in the error message

You can compare error variables with the equality operator `==`:
If you need to construct a more complex error message from some variables, you can make use of string formatting.
Go has a special `fmt.Errorf` function for this purpose.
It constructs the error message string and returns an error.

```go
var ErrResourceNotFound = errors.New("resource not found")
// ...
if err == ErrResourceNotFound {
// do something about the resource-not-found error
}
input := 123
action := "UPDATE"

err := fmt.Errorf("invalid input %d for action %s", input, action)
err.Error()
// => "invalid input 123 for action UPDATE"
```

How to check for errors of a specific custom error type will be covered in later concepts.
## Error checking

## Best Practice: Return Early
If you call a function that returns an error, it is common to store the error value in a variable called `err`.
Before you use the actual result of the function, you need to check that there was no error.
We can use `==` and `!=` to compare the error against `nil` and we know there was an error when `err` is not `nil`.

It is best practice to return early when an error is encountered.
That avoids nesting the "happy path" code, see this [article by Mat Ryer][line-of-sight] for more details.
To avoid nesting the "happy path" of your code, error cases should be handled first, usually via an early return.
See this famous [article by Mat Ryer][line-of-sight] for more details.

```go
// Do this:
Expand Down Expand Up @@ -135,29 +127,42 @@ func myFunc() error {
}
```

## Scope of Errors
Most of the time, the error will be returned up the function stack as shown in first example above.
Another way of handling the error could be to log it and continue with some other operation.
It is good practice to either return or log the error, never both.

In Go, it is also a best practice to avoid declaring variables in a scope if these are not required later.
Since most functions in Go include an error as one of the return values, you will see/use the `if err != nil` pattern all over the place in Go code.

## Custom error types

If you want your error to include more information than just the error message string, you can create a custom error type.
As mentioned before, everything that implements the `error` interface (i.e. has an `Error() string` method) can serve as an error in Go.

For the sake of this example, we want to check for errors only:
Usually, a struct is used to create a custom error type.
By convention, custom error type names should end with `Error`.
Also, it is best to set up the `Error() string` method with a pointer receiver, see this [Stackoverflow comment][stackoverflow-errors] to learn about the reasoning.
Note that this means you need to return a pointer to your custom error otherwise it will not count as `error` because the non-pointer value does not provide the `Error() string` method.

```go
_, err := os.Getwd()
if err != nil {
return nil, err
type MyCustomError struct {
message string
details string
}
// at this point, err is still visible.
```

So this is the preferred way to consume this error:
func (e *MyCustomError) Error() string {
return fmt.Sprintf("%s, details: %s", e.message, e.details)
}

```go
if _, err := os.Getwd(); err != nil {
return
func someFunction() error {
// ...
return &MyCustomError{
message: "...",
details: "...",
}
}
// err is now out of this scope.
```

[stackoverflow-errors]: https://stackoverflow.com/a/50333850
[line-of-sight]: https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88
[buffer-write]: https://pkg.go.dev/bytes#Buffer.Write

Loading

0 comments on commit 373215e

Please sign in to comment.