DEV Community

Cover image for Errors are Values: A Guide to Error Handling in Go
Fred
Fred

Posted on • Edited on

Errors are Values: A Guide to Error Handling in Go

"Errors are values" embodies a core design philosophy in Go's error handling system, setting it apart from many other languages.

Imagine a car:
Other languages(Exception-Based): A car's engine suddenly explodes, and the car stops immediately. You have no choice but to address the engine problem before you can proceed.
Go(Error as Value): A car's low fuel light comes on, but the engine keeps running. You can make a choice: stop for gas, or try to reach a gas station. The car's performance is impacted, but you can control your options.

Why Go Rocks:

  • Control: You, as the developer, have total control over how errors are handled, preventing code execution from crashing abruptly.
  • Explicit Decisions: Every error is explicitly checked. You are forced to consider what actions should be taken (try again, log it, recover or exit).
  • Simple and Elegant: Error values fit within Go's clean syntax and contribute to clean code.

1. The error Type

In go, errors are represented using the built-in error type.
It is an interface with a single method.

Example:

type error interface {
    Error() string
}
Enter fullscreen mode Exit fullscreen mode

Any type that implements the Error() method qualifies as an error.

2. Custom Errors

fmt.Errorf is used to create an error. The function may return both the result and the error.The error message should start with small caps.

Example:

package main

import (
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide %d by %d", a, b)
    }
    return a / b, nil
}
Enter fullscreen mode Exit fullscreen mode

3. Handling Errors

When the function returns two values(result, err), you can explicitly check the second value; err != nil and if there's an error, you can handle it.

Example:

package main

import (
    "fmt"
)

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Custom Error Types

You can create a custom error by defining a struct that implements the error interface. Learn about interfaces here

Example:

package main

import (
    "fmt"
)

type DivideByZeroError struct {
    message string
}

func (e *DivideByZeroError) Error() string{
    return e.Message
}

func customDivide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivideByZeroError{"cannot divide by zero"}
    }
    return a / b, nil
}
Enter fullscreen mode Exit fullscreen mode

5. Wrapping and Unwrapping Errors

Since release of Go 1.13, the errors package provides utilities for wrapping and unwrapping errors, enabling adding more context to errors.

  • fmt.Errorf is used with %w to wrap the original error for context.
  • errors.Unwrap extracts the original error wrapped with another error.

Example:

package main

import (
    "fmt"
    "errors"
)

func openFile(filename string) error {
    return fmt.Errorf("openFile: failed to read file %s: %w",filename, errors.New("file not found"))
}

func main() {
    err := openFile("test.txt")
    if err != nil {
        if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil {
            fmt.Println("Original error:", unwrappedErr)
        }
        fmt.Println("Error:", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Panic

gophers panicking
You can handle severe errors using panic. This is for unexpected situations that your program cannot recover from. When a function panics, it stops execution and begins unwinding the stack, running deferred functions in reverse order.

Example:

package main

import "fmt"

func doPanic() {
    panic("something went wrong")
}

func main() {
    fmt.Println("Starting...")
    doPanic()
    fmt.Println("This will never be printed")
}
Enter fullscreen mode Exit fullscreen mode

7. Handling Panic

You can recover from a panic by using defer and recover.

Defer : The defer keyword schedules a function to be executed after the surrounding function returns, or in case of a panic, before the stack unwinds. Deferred functions clean up resources or handle any final tasks.

Example:

package main

import "fmt"

func cleanup() {
    fmt.Println("Clean up done.")
}

func riskyOperation() {
    defer cleanup()
    panic("Something went wrong")
}

func main() {
    riskyOperation()
    fmt.Println("Main finished")  // This will not be printed because of the panic
}
Enter fullscreen mode Exit fullscreen mode

In this example, the cleanup function is deferred and will run before the program crashes, but the "Main finished" message will never be printed because the panic stops the normal flow.

Recover: recover is used to catch a panic and prevent it from terminating the program. It is typically used in a deferred function.

Example:

package main

import "fmt"

func riskyOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    panic("A critical failure")
}

func main() {
    riskyOperation()
    fmt.Println("Main continues normally after recovering from panic")
}
Enter fullscreen mode Exit fullscreen mode

In this case, the recover function catches the panic and the program can continue running after the panic.

8. Error Handling Best Practices:

  1. Always check for errors: In Go, ignoring errors is possible but discouraged.
  2. Return wrapped errors for context: This helps in debugging by providing more detail about the error path.
  3. Avoid using panic for regular error handling: It's better to return errors using the error type and let the caller decide how to handle them.
  4. Libraries should not use panic for expected errors: Instead, they should return error values, allowing the client code to handle them.

Conclusion

Go's error handling approach helps developers like you write reliable and easy-to-maintain programs. It gives you the flexibility to manage errors in a way that fits your application, leading to clearer and more robust code.

Gopher mascot dropping mic as it exit stage

Top comments (3)

Collapse
 
denilany profile image
Denil

So many ways to handle errors, I'm really impressed by the custom error part.

Collapse
 
fredgitonga profile image
Fred

yes,error handling is go's forte. Custom errors allow you to add more context , this is a huge plus when debugging. Glad you enjoyed @denilany

Collapse
 
githaiga22 profile image
Allan Githaiga

nice article