Golang Errors And Custom Error Types Complete Guide
Understanding the Core Concepts of GoLang Errors and Custom Error Types
GoLang Errors and Custom Error Types
Understanding Go's Error Handling
Go treats errors as first-class citizens, meaning that it encourages explicitly handling errors wherever they occur. The native error
type in Go is an interface defined in the builtin
package:
type error interface {
Error() string
}
This simple interface design allows any type that implements the Error()
method to be used as an error.
Basic Error Handling
Here's a basic example of how to use built-in errors:
package main
import (
"fmt"
"math"
)
func Sqrt(value float64) (float64, error) {
if value < 0 {
return 0, fmt.Errorf("square root of negative value")
}
return math.Sqrt(value), nil
}
func main() {
result, err := Sqrt(-1)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
In this example, the Sqrt
function returns a tuple containing the result and an error. If there's an error, the error value will be non-nil, and we handle it accordingly in the main
function.
Creating Custom Error Types
Custom error types can provide additional context and specificity to error handling in Go. They allow you to define errors that carry extra information or methods that can be used for processing or logging. Here's how to create and use custom error types:
- Struct Error Type: Define a struct that satisfies the
error
interface.
Online Code run
Step-by-Step Guide: How to Implement GoLang Errors and Custom Error Types
1. Basic Error Handling
Example: Reading a File with Error Checking
Let's start with the most common form of error handling in Go, using the built-in error
type and checking if an error occurred after a function call.
package main
import (
"fmt"
"io/ioutil"
"log"
)
func readFile(filename string) ([]byte, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file %q: %w", filename, err)
}
return data, nil
}
func main() {
filename := "example.txt"
data, err := readFile(filename)
if err != nil {
log.Fatalf("Error occurred while reading the file %v\n", err)
}
fmt.Printf("File data:\n%s\n", data)
}
Explanation:
- readFile: This function reads the content of a file and returns the data as a byte slice and an error.
ioutil.ReadFile
reads the file contents and returns the data and an error.fmt.Errorf
formats the error with a message and wraps the original error, making debugging easier.
- main: Calls
readFile
and checks for errors.- If an error occurs, the program prints the error and exits with
log.Fatalf
. - Otherwise, it prints the contents of the file.
- If an error occurs, the program prints the error and exits with
2. Custom Error Types
Example: Defining a Custom Error Type
Sometimes you need more information about the error than is provided by the default error
type. You can define your own.
package main
import (
"errors"
"fmt"
"log"
)
// CustomError represents a specific kind of error.
type CustomError struct {
Code int
Message string
}
// Error returns the string representation of the error.
func (e *CustomError) Error() string {
return fmt.Sprintf("code: %d, message: %s", e.Code, e.Message)
}
// NewCustomError creates a new instance of CustomError.
func NewCustomError(code int, message string) *CustomError {
return &CustomError{
Code: code,
Message: message,
}
}
func validateInput(input string) error {
if input == "" {
return NewCustomError(400, "input is empty")
}
if len(input) > 10 {
return NewCustomError(413, "input too long")
}
return nil
}
func performOperation(input string) error {
err := validateInput(input)
if err != nil {
return err
}
// Simulate an operation that could fail
if input == "invalid" {
return NewCustomError(500, "operation failed")
}
fmt.Println("Operation successful")
return nil
}
func main() {
input := ""
err := performOperation(input)
if e, ok := err.(*CustomError); ok {
log.Fatalf("Custom Error: code=%d, message=%q\n", e.Code, e.Message)
} else if err != nil {
log.Fatalf("Unexpected Error: %v\n", err)
}
}
Explanation:
- CustomError: A struct that contains more information about the error, such as a code and a message.
- Error() Method: Required to implement the
error
interface, it returns a string describing the error. - NewCustomError Function: Helps in creating instances of
CustomError
, encapsulating initialization logic. - validateInput Function: Checks the validity of the input and returns a custom error if it fails.
- performOperation Function: Checks the input using
validateInput
and performs an operation that might fail, returning a custom error if necessary. - main Function: Calls
performOperation
and checks for errors of the typeCustomError
to handle them differently.
3. Creating Custom Error Types Using Struct Embedding
Example: Embedding Errors
In some cases, it is useful to embed errors within other custom error types to retain original information.
package main
import (
"errors"
"fmt"
"log"
)
// OperationError wraps a generic error and provides additional context.
type OperationError struct {
OriginalErr error
Context string
}
// Error implements the error interface.
func (e *OperationError) Error() string {
if e.Context != "" {
return fmt.Sprintf("%s | Original Error: %v", e.Context, e.OriginalErr)
}
return fmt.Sprintf("Original Error: %v", e.OriginalErr)
}
// Is compares two errors and returns true if they match.
func (e *OperationError) Is(target error) bool {
return errors.Is(e.OriginalErr, target)
}
func performDatabaseOperation(input string) error {
// Assume some database operation happens here.
if input == "invalid" {
return errors.New("database operation failed due to invalid input")
}
return nil
}
func main() {
input := "invalid"
err := performDatabaseOperation(input)
if err != nil {
opErr := &OperationError{
OriginalErr: err,
Context: "Error during database operation",
}
log.Fatalf("Error: %v\n", opErr)
}
}
Explanation:
- OperationError: A custom error type that embeds another error (
OriginalErr
) and adds context (Context
). - Is Method: Allows comparing
OperationError
with the underlying error usingerrors.Is
. - performDatabaseOperation: Simulates a database operation that can fail.
- main: Calls
performDatabaseOperation
and wraps any returned errors inOperationError
.
4. Using the errors
Package for Sentinel Values
Example: Sentinel Value Error Handling
Go provides the errors
package which includes the errors.New
function to create sentinel values, i.e., predefined errors.
package main
import (
"errors"
"fmt"
)
// Define sentinel errors for this example
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
// GetUser fetches user data from a database.
func GetUser(username string) (string, error) {
if username == "" {
return "", ErrNotFound
}
if username == "admin" {
return "", ErrUnauthorized
}
return fmt.Sprintf("User %s fetched", username), nil
}
func main() {
username := ""
userData, err := GetUser(username)
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found.")
} else if errors.Is(err, ErrUnauthorized) {
fmt.Println("Access denied.")
} else if err != nil {
fmt.Printf("Unexpected Error: %v\n", err)
} else {
fmt.Println(userData)
}
username = "admin"
userData, err = GetUser(username)
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found.")
} else if errors.Is(err, ErrUnauthorized) {
fmt.Println("Access denied.")
} else if err != nil {
fmt.Printf("Unexpected Error: %v\n", err)
} else {
fmt.Println(userData)
}
}
Explanation:
- Sentinel Errors:
ErrNotFound
andErrUnauthorized
are defined as sentinel errors. - GetUser: Fetches a user based on the username and returns appropriate sentinel errors.
- main: Uses
errors.Is
to check the specific type of error and handles each one accordingly.
5. Custom Error Type with Multiple Fields
Example: Rich Custom Error Types
You can create a custom error type that has multiple fields providing detailed information.
package main
import (
"errors"
"fmt"
"log"
)
// APIError represents errors encountered while interacting with an API.
type APIError struct {
status int
code int
message string
}
// Error returns the string representation of the error.
func (e *APIError) Error() string {
return fmt.Sprintf("API Error: status=%d, code=%d, message=%q", e.status, e.code, e.message)
}
// Is compares two errors and returns true if they match.
func (e *APIError) Is(target error) bool {
var te *APIError
oerr, ok := target.(*APIError)
if !ok {
return false
}
te = oerr
return te.status == e.status && te.code == e.code && te.message == e.message
}
// NewAPIError creates a new instance of APIError.
func NewAPIError(status, code int, message string) *APIError {
return &APIError{
status: status,
code: code,
message: message,
}
}
func callAPI(input string) error {
if input == "" {
return NewAPIError(400, 1001, "input validation failed")
}
if input == "unknown" {
return NewAPIError(404, 1002, "resource not found")
}
return nil
}
func main() {
input := "unknown"
err := callAPI(input)
if err != nil {
apiErr, ok := err.(*APIError)
if ok {
switch {
case errors.Is(apiErr, NewAPIError(400, 1001, "")):
log.Fatalf("Bad Request: %v\n", apiErr)
case errors.Is(apiErr, NewAPIError(404, 1002, "")):
log.Fatalf("Resource Not Found: %v\n", apiErr)
default:
log.Fatalf("Another API Error: %v\n", apiErr)
}
} else {
log.Fatalf("Unexpected Error: %v\n", err)
}
}
log.Println("API call was successful")
}
Explanation:
- APIError Types: Contains detailed fields — HTTP status, error code, and an error message.
- Error Method: Returns a formatted string containing all error details.
- Is Method: Allows comparison of different instances of
APIError
. - NewAPIError Function: Creates instances of
APIError
. - callAPI Function: Simulates an API call and returns appropriate
APIError
sentinel values. - main: Calls
callAPI
and checks the specific type ofAPIError
usingerrors.Is
.
6. Wrapping Errors with Context
Example: Adding Context to Errors
In more complex applications, it is useful to add context when wrapping errors.
Top 10 Interview Questions & Answers on GoLang Errors and Custom Error Types
Top 10 Questions and Answers on GoLang Errors and Custom Error Types
1. What is an error in Go? How do you handle errors?
Example:
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error occurred:", err)
} else {
fmt.Printf("Result: %d\n", result)
}
2. Can you show me how to create a custom error type?
Answer: Yes, a custom error type can be defined by creating a struct that implements the Error()
method.
Example:
type MyError struct {
Msg string
}
func (e MyError) Error() string {
return e.Msg
}
func operation(val int) (int, error) {
if val < 0 {
return 0, MyError{"Negative value not allowed"}
}
return val * 2, nil
}
Here, MyError
is a custom error type that can be used to provide more detailed error information when a function fails.
3. How do you wrap or extend an existing error in Go?
Answer: GoLang’s standard library provides errors.Wrap
from the github.com/pkg/errors
package which allows for wrapping an existing error to add more context. As of Go 1.13, Go's own error handling includes fmt.Errorf("%w", ...)
for wrapping errors.
Using github.com/pkg/errors
:
err := operation(-1)
if err != nil {
return errors.Wrap(err, "failed to perform operation")
}
Using Go's native error wrapping:
err := operation(-1)
if err != nil {
return fmt.Errorf("failed to perform operation: %w", err)
}
4. How do you check the type of a wrapped error?
Answer: To check the type of a wrapped error in Go 1.13+, you can use the errors.Is()
, errors.As()
functions along with fmt.Errorf("%w", ...)
.
errors.Is(err, target)
: checks if the error chain contains an error that matches the target.errors.As(err, &target)
: looks through the error chain and finds the first error that matches the target's type.
Example:
err := operation(-1)
var myErr MyError
if errors.As(err, &myErr) {
fmt.Println("Detected MyError in chain")
}
5. Can you explain how the errors.Join
function works?
Answer: Introduced in Go 1.20, the errors.Join
function concatenates multiple errors into a single error interface. It takes variadic arguments and returns a new error that combines all the given errors. If there are no errors passed, it returns nil
.
Example:
err1 := errors.New("error one")
err2 := errors.New("error two")
joinedErr := errors.Join(err1, err2)
fmt.Println(joinedErr) // prints "error one\nerror two"
6. What is the panic()
function in Go? When should it be used?
Answer: The panic()
function is used to signal an exceptional run-time condition. A program panics when something unexpected happens and it cannot continue normally. After a panic, it will crash the program unless recovered using recover()
in a deferred function call. panic()
should be rare and only used as a last resort, such as a critical failure that affects the entire program state.
Example:
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
if conditionToPanic {
panic("Something went really wrong")
}
7. Can you provide an example of using recover()
?
Answer: recover()
is used within deferred functions to regain control after a panic. It stops the panic process and returns the value that was passed to panic()
.
Example:
func riskyOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Operation panicked with:", r)
}
}()
panic("Oops!")
}
func main() {
riskyOperation()
fmt.Println("Program continues despite the panic.")
}
8. Can errors be compared directly in Go? If not, how should they be compared?
Answer: Directly comparing errors using ==
is usually not recommended because most errors in Go are dynamically created, resulting in different memory addresses. Therefore, to compare errors you can:
- Use sentinel errors: Define specific errors as exported variables and compare them directly using
==
. - Check error chains: Use
errors.Is(err, targetError)
to see if the error chain contains the target error.
Sentinel error example:
var ErrNotFound = errors.New("not found")
func lookup(id string) error {
if id == "" {
return ErrNotFound
}
// logic here...
return nil
}
// elsewhere...
err := lookup("")
if err == ErrNotFound {
fmt.Println("ID not found,", err)
}
9. How can I format error messages in Go?
Answer: To format error messages, you use the fmt.Errorf()
function to interpolate strings and other values into your error messages.
Example:
func createFile(name string) error {
_, err := os.Create(name)
if err != nil {
return fmt.Errorf("failed to create file %s: %w", name, err)
}
return nil
}
// elsewhere...
err := createFile("example.txt")
if err != nil {
fmt.Println("Error:", err)
}
This example uses %w
to wrap the underlying error which can then be used with functions like errors.Is()
and errors.As()
.
10. When should you return an error versus panic in Go?
Answer: You should return an error in situations where you expect something could go wrong and that the caller of your function knows best how to handle that failure. This is the idiomatic way of Go’s error handling.
On the other hand, you should use panic()
in rare circumstances where continuing to execute the program would be meaningless or dangerous, such as during the initialization of a package, when encountering a fatal programming error, or when a function cannot continue execution due to an irrecoverable situation that should stop the application completely.
In general, if the operation is one you expect to fail on occasion and want to handle failures gracefully, use errors. If a failure is catastrophic and should halt the program entirely, use panics.
Example of returning an error:
func readFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err // Handle error at the caller site
}
defer file.Close()
content, err := ioutil.ReadAll(file)
return content, err // Handle error at the caller site
}
And an example of using panic()
:
Login to post a comment.