Go > Error Handling > Built-in Error Interface > Custom error types

Custom Error Types in Go

This example demonstrates how to define and use custom error types in Go, enhancing error handling and providing more context to the caller.

Defining a Custom Error Type

This code defines a custom error type called InsufficientFundsError. It's a struct containing fields relevant to the error condition: the account ID, the requested amount, and the current balance. The key to making this a proper error type is implementing the Error() method. This method returns a string representation of the error, conforming to the error interface.

package main

import (
	"fmt"
)

type InsufficientFundsError struct {
	AccountID   string
	RequestedAmount float64
	CurrentBalance  float64
}

func (e *InsufficientFundsError) Error() string {
	return fmt.Sprintf("account %s has insufficient funds. Requested: %.2f, Available: %.2f", e.AccountID, e.RequestedAmount, e.CurrentBalance)
}

Using the Custom Error Type

This code defines a withdraw function that can return either the new balance or an InsufficientFundsError. The function checks if the requested amount exceeds the current balance. If it does, it creates a new InsufficientFundsError instance with the relevant details and returns it. In the main function, the code calls withdraw and checks for an error. If an error occurs, it uses errors.As to check if the error is of type InsufficientFundsError. If so, the specific error details (account ID, requested amount, and current balance) are printed. This is much more informative than just a generic error message.

package main

import (
	"errors"
	"fmt"
)

type InsufficientFundsError struct {
	AccountID   string
	RequestedAmount float64
	CurrentBalance  float64
}

func (e *InsufficientFundsError) Error() string {
	return fmt.Sprintf("account %s has insufficient funds. Requested: %.2f, Available: %.2f", e.AccountID, e.RequestedAmount, e.CurrentBalance)
}

func withdraw(accountID string, amount float64, balance float64) (float64, error) {
	if amount > balance {
		return balance, &InsufficientFundsError{AccountID: accountID, RequestedAmount: amount, CurrentBalance: balance}
	}
	return balance - amount, nil
}

func main() {
	newBalance, err := withdraw("12345", 100.00, 50.00)
	if err != nil {
		var insufficientFundsErr *InsufficientFundsError
		if errors.As(err, &insufficientFundsErr) {
			fmt.Println("Withdrawal failed:", insufficientFundsErr)
			fmt.Printf("Account ID: %s\n", insufficientFundsErr.AccountID)
			fmt.Printf("Requested Amount: %.2f\n", insufficientFundsErr.RequestedAmount)
			fmt.Printf("Current Balance: %.2f\n", insufficientFundsErr.CurrentBalance)
		} else {
			fmt.Println("Withdrawal failed with a generic error:", err)
		}
		return
	}
	fmt.Println("Withdrawal successful. New balance:", newBalance)
}

Concepts Behind the Snippet

The core concept is that Go's error interface is satisfied by any type that implements the Error() string method. By creating custom structs and implementing this method, you can create error types that carry specific data related to the error. This allows for more detailed error handling and reporting. The errors.As function is used to determine if an error is of a specific type. This allows you to handle different types of errors differently.

Real-Life Use Case

Imagine a system that processes financial transactions. Different types of errors can occur: insufficient funds, invalid account number, transaction timeout, etc. Using custom error types, you can represent each of these errors as distinct types, each carrying relevant information. For example, a TransactionTimeoutError might include the transaction ID and the timestamp of the timeout, allowing for more sophisticated debugging and retry logic.

Best Practices

  • Provide Context: Custom error types should include all relevant data to understand the error.
  • Use errors.As: Use errors.As to check for specific error types. Avoid type assertions directly, as they can lead to panics.
  • Wrap Errors (Optional): Consider wrapping errors using fmt.Errorf("%w", originalError) to preserve the original error's context while adding more information.
  • Don't Ignore Errors: Always handle errors appropriately. Ignoring errors can lead to unexpected behavior and difficult-to-debug issues.

Interview Tip

Be prepared to explain the benefits of custom error types over simple strings. Emphasize the ability to carry specific error data and the improved error handling capabilities that come with it. Also, understand the difference between errors.Is and errors.As. errors.Is compares errors directly, while errors.As checks if an error implements a specific interface or is of a specific type.

When to Use Them

Use custom error types when you need to provide more information about an error than a simple string can convey. They're particularly useful when the calling code needs to make decisions based on the specific type of error that occurred. Use simple errors when you only need to indicate that an error occurred without any specific context.

Memory Footprint

The memory footprint of a custom error type depends on the size of the fields it contains. A struct with a few string and numeric fields will typically have a small memory footprint. Avoid including large data structures in your error types unless absolutely necessary, as this can increase memory consumption and potentially impact performance.

Alternatives

  • String Errors: Simple to use, but lack context.
  • Error Wrapping: Can add context to existing errors, but doesn't provide type safety.
  • Sentinel Errors: Predefined error variables; simple but can lead to brittle code (using errors.Is).

Pros

  • Context-Rich: Provide detailed information about the error.
  • Type-Safe: Allow for specific error handling based on error type.
  • Improved Debugging: Easier to diagnose and fix errors due to richer information.

Cons

  • More Complex: Require more code to define and use compared to simple string errors.
  • Potential for Over-Engineering: Can be overkill for simple error scenarios.

FAQ

  • When should I use a custom error type vs. a simple error string?

    Use custom error types when you need to provide more context about the error, such as specific details that the calling function can use to make decisions. Use simple error strings when you only need to indicate that an error occurred without any specific context.
  • How do I check if an error is of a specific custom type?

    Use the errors.As function to check if an error is of a specific custom type. This function safely checks if the error implements the target interface or is of the target type.
  • What is the purpose of the Error() string method?

    The Error() string method is what makes a type an error in Go. Any type that implements this method can be returned as an error value. The method should return a human-readable string representation of the error.