Error handling in Golang
Tags: go, programming
Published: 2023-09-08 02:11:23

Error handling in Go is somewhat unique compared to some other programming languages. Instead of using exceptions, Go uses error values to indicate an abnormal state. This design promotes explicit error checking and can lead to more robust code.

## 1. The `error` Type:
Go has a built-in interface type called `error`. This interface has a single method `Error() string`.

```go
type error interface {
    Error() string
}
```

> Any type that implements this method can be used as an error.

## 2. Returning Errors:
It's idiomatic in Go to return errors from functions and methods. A common pattern is to return both a result and an error, and to check the error immediately:

```go
func SomeFunction() (ResultType, error) {
    if somethingWrong {
        return nil, errors.New("something went wrong")
    }
    return result, nil
}

result, err := SomeFunction()
if err != nil {
    // handle the error
}
```

## 3. Custom Error Types:

Custom error types can be particularly useful when you want to provide more information about an error or categorize errors in a way that allows the caller to programmatically react to different error scenarios.

Here's an example of how you might use custom error types in Go:

```go
package main

import (
    "fmt"
)

// NotFoundError represents an error when something is not found.
type NotFoundError struct {
    EntityName string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.EntityName)
}

// PermissionError represents an error when permissions are denied.
type PermissionError struct {
    OperationName string
}

func (e *PermissionError) Error() string {
    return fmt.Sprintf("permission denied for %s", e.OperationName)
}
```

### Using and Checking Custom Error Types:

```go
func FindEntity(name string) (string, error) {
    // Simulating some logic
    if name == "missingEntity" {
        return "", &NotFoundError{EntityName: name}
    }
    return "EntityData", nil
}

func PerformOperation(name string) error {
    // Simulating some logic
    if name == "restrictedOperation" {
        return &PermissionError{OperationName: name}
    }
    return nil
}

func main() {
    _, err := FindEntity("missingEntity")
    if err != nil {
        switch e := err.(type) {
        case *NotFoundError:
            fmt.Println("Handle not found:", e.Error())
        case *PermissionError:
            fmt.Println("Handle permission error:", e.Error())
        default:
            fmt.Println("Handle general error:", e.Error())
        }
    }

    err = PerformOperation("restrictedOperation")
    if err != nil {
        switch e := err.(type) {
        case *NotFoundError:
            fmt.Println("Handle not found:", e.Error())
        case *PermissionError:
            fmt.Println("Handle permission error:", e.Error())
        default:
            fmt.Println("Handle general error:", e.Error())
        }
    }
}
```

In this example, we have two custom error types: `NotFoundError` and `PermissionError`. When we encounter these errors in our functions (`FindEntity` and `PerformOperation`), we return instances of these types. In our main function, we can use a type switch to handle different types of errors in different ways.

This approach provides flexibility and clarity in error handling, and allows callers to react differently based on the type of error encountered.

## 4. The `fmt` Package for Errors:
The `fmt` package can be used to format error strings. The Errorf function is particularly handy for this:

```go
return fmt.Errorf("error occurred while processing %s", itemName)
```

## 5. Wrapping Errors:

As of Go 1.13, the standard library added support for wrapping errors using the `%w` verb with `fmt.Errorf`. This allows you to wrap an error with additional context, while preserving the original error:

```go
if err != nil {
    return fmt.Errorf("failed processing: %w", err)
}
```

You can then later unwrap and check the underlying error using `errors.Is` and `errors.As`.

## 6. Checking Errors:

The `errors` package provides utilities to work with errors.

`errors.Is`: Check if an error is a specific error or wrapped version of it.
`errors.As`: Check if an error matches a certain type and, if so, get the typed value.

```go
var specificErr MyError
if errors.As(err, &specificErr) {
    // handle specificErr
}
```

## Best Practices:

- Always handle errors explicitly. Don't ignore them unless you're certain it's safe to do so.
- When wrapping errors, provide context so that the root cause can be traced.
- Avoid having too many error return values. If a function returns multiple errors, consider if there's a way to simplify or break up the function.
- Use custom error types judiciously. Often, a simple error string is sufficient.

By following these practices and understanding Go's unique approach to error handling, you can write more resilient and maintainable code.