Golang Errors And Custom Error Types Complete Guide

 Last Update:2025-06-22T00:00:00     .NET School AI Teacher - SELECT ANY TEXT TO EXPLANATION.    11 mins read      Difficulty-Level: beginner

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:

  1. Struct Error Type: Define a struct that satisfies the error interface.

Online Code run

🔔 Note: Select your programming language to check or run code at

💻 Run Code Compiler

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.

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 type CustomError 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 using errors.Is.
  • performDatabaseOperation: Simulates a database operation that can fail.
  • main: Calls performDatabaseOperation and wraps any returned errors in OperationError.

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 and ErrUnauthorized 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 of APIError using errors.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():

You May Like This Related .NET Topic

Login to post a comment.