Custom Errors in Go

Error handling in Go is crucial for building robust applications. By defining and using custom error types, developers can provide more context and specificity in error handling, thereby improving diagnostics and facilitating better error propagation. This section explores the creation of custom error types and the usage of error wrapping and unwrapping techniques using the fmt and errors packages.

Defining Custom Error Types

Creating custom error types in Go allows for more descriptive and context-specific error messages. This approach helps in distinguishing between different kinds of errors, making it easier to handle them appropriately.

Example: Defining a Custom Error Type

go
package main import ( "fmt" ) // Define a custom error type type MyError struct { Code int Message string } func (e *MyError) Error() string { return fmt.Sprintf("Error %d: %s", e.Code, e.Message) } func doSomething() error { // Simulate an error return &MyError{Code: 123, Message: "Something went wrong"} } func main() { err := doSomething() if err != nil { fmt.Println(err) } }

In this example, MyError is a custom error type that includes an error code and a message. The Error method satisfies the error interface, enabling instances of MyError to be treated as standard errors.

Error Wrapping and Unwrapping

Error wrapping is a technique used to add context to an error while preserving the original error. This is particularly useful for error propagation, allowing each layer of the application to add relevant context.

Example: Wrapping an Error

go
package main import ( "errors" "fmt" ) func doSomething() error { return errors.New("original error") } func doSomethingElse() error { err := doSomething() if err != nil { return fmt.Errorf("doSomethingElse failed: %w", err) } return nil } func main() { err := doSomethingElse() if err != nil { fmt.Println(err) } }

In this example, the fmt.Errorf function is used with the %w verb to wrap the original error with additional context. This allows the doSomethingElse function to add its own message while preserving the original error for further inspection.

Unwrapping Errors

To retrieve the original error, the errors.Unwrap function or the errors.As and errors.Is functions can be used.

Example: Unwrapping an Error

go
package main import ( "errors" "fmt" ) func doSomething() error { return errors.New("original error") } func doSomethingElse() error { err := doSomething() if err != nil { return fmt.Errorf("doSomethingElse failed: %w", err) } return nil } func main() { err := doSomethingElse() if err != nil { fmt.Println("Error occurred:", err) if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil { fmt.Println("Unwrapped error:", unwrappedErr) } } }

Here, errors.Unwrap retrieves the original error that was wrapped, allowing for deeper inspection and more granular error handling.

Using errors.As and errors.Is

The errors.As function is used to check if an error can be assigned to a specific type, while errors.Is is used to compare an error to a target error.

Example: Using errors.As

go
package main import ( "errors" "fmt" ) type MyError struct { Code int Message string } func (e *MyError) Error() string { return fmt.Sprintf("Error %d: %s", e.Code, e.Message) } func doSomething() error { return &MyError{Code: 123, Message: "something went wrong"} } func main() { err := doSomething() var myErr *MyError if errors.As(err, &myErr) { fmt.Printf("Custom error occurred: Code=%d, Message=%s\n", myErr.Code, myErr.Message) } else { fmt.Println("An error occurred:", err) } }

In this example, errors.As checks if the error is of type *MyError and, if so, extracts the custom error details.

Example: Using errors.Is

go
package main import ( "errors" "fmt" ) var ErrSpecific = errors.New("specific error") func doSomething() error { return fmt.Errorf("something went wrong: %w", ErrSpecific) } func main() { err := doSomething() if errors.Is(err, ErrSpecific) { fmt.Println("A specific error occurred:", err) } else { fmt.Println("An error occurred:", err) } }

In this example, errors.Is checks if the error matches ErrSpecific, allowing for specific error handling.

Conclusion

Defining and using custom error types in Go enhances error handling by providing more context and specificity. Error wrapping and unwrapping techniques, facilitated by the fmt and errors packages, further improve error diagnostics and propagation. By leveraging these mechanisms, developers can build more resilient and maintainable applications.

Becoming a Senior Go Developer: Mastering Go and Its Ecosystem