Skip to main content

Overview

In many languages (Java, Python, JavaScript), errors are “exceptions” — they crash the program unless you catch them. In Go, Errors are Values. They are just like int or string. You don’t “throw” them; you pass them around. This philosophy forces you to handle failure cases explicitly where they happen, rather than letting them bubble up and crash your app unexpectedly.

Core Philosophy: Errors as Values

Go treats errors as normal return values. Every function that can fail returns two things:
  1. The result (if successful)
  2. An error (if something went wrong)
result, err := someFunction()
if err != nil {
    // Handle the error
}
// Use result
The error type is simply an interface with a single method:
type error interface {
    Error() string
}
Anything that implements this interface is an error.

Flow Comparison

Sentinel Errors

Think of sentinel errors as “Error Constants” — pre-defined error values that you check for specific conditions.

Defining Sentinel Errors

errorhandling/main.go
var ErrNotFound = errors.New("item not found")

Using Sentinel Errors

errorhandling/main.go
func findItem(id int) (string, error) {
	if id == 42 {
		return "Everything", nil
	}
	// Return the sentinel error
	return "", ErrNotFound
}

Checking for Sentinel Errors with errors.Is

Always use errors.Is() instead of == for error comparison. This handles wrapped errors correctly.
errorhandling/main.go
item, err := findItem(100)
if err != nil {
	// Use errors.Is to check for specific sentinel errors
	if errors.Is(err, ErrNotFound) {
		fmt.Println("Search failed: The item does not exist.")
	} else {
		fmt.Println("Search failed: Unknown error.")
	}
} else {
	fmt.Println("Found:", item)
}
Analogy: Sentinel errors are like checking a specific HTTP status code (404, 500, etc.).

Custom Error Types

Sometimes a simple string isn’t enough. You need context: “Which URL failed?”, “How many retries were attempted?”. Custom error types allow you to attach structured data to errors.

Defining a Custom Error Type

errorhandling/main.go
type ConnectionError struct {
	URL      string
	Attempts int
}

func (e *ConnectionError) Error() string {
	return fmt.Sprintf("failed to connect to %s after %d attempts", e.URL, e.Attempts)
}
1
Step 1: Define the struct
2
Create a struct that holds the additional context you need.
3
Step 2: Implement the Error() method
4
This makes your struct satisfy the error interface.
5
Step 3: Return the custom error
6
Return a pointer to your error struct when the operation fails.

Returning Custom Errors

errorhandling/main.go
func connectToService(url string) error {
	// Simulate a failure
	return &ConnectionError{URL: url, Attempts: 3}
}

Extracting Data with errors.As

Use errors.As to “unwrap” the error and access the fields inside the struct.
errorhandling/main.go
err = connectToService("http://example.com")
if err != nil {
	var connErr *ConnectionError
	if errors.As(err, &connErr) {
		fmt.Printf("Connection Error Details -> URL: %s | Retried %d times\n", 
			connErr.URL, connErr.Attempts)
	} else {
		fmt.Println("Generic error:", err)
	}
}

Basic Error Handling Pattern

Here’s a complete example showing the fundamental pattern:
errorhandling/task1/main.go
func getAge(m map[string]int, name string) (int, error) {
	if m == nil {
		return 0, errors.New("map is nil")
	}

	age, ok := m[name]
	if !ok {
		return 0, errors.New("name not found")
	}

	return age, nil
}

func main() {
	ages := map[string]int{
		"Alice": 25,
		"Bob":   30,
	}

	age, err := getAge(ages, "Alice")
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Println("age:", age)
}

errors.Is vs errors.As

// Use errors.Is to check if an error matches a specific value
if errors.Is(err, ErrNotFound) {
    // This specific error occurred
}
FunctionPurposeWhen to Use
errors.IsCheck if error matches a specific sentinel errorComparing against pre-defined error constants
errors.AsExtract and type-assert custom error structsWhen you need to access fields in a custom error type

Key Takeaways

1
Errors are not exceptions
2
They’re normal return values that must be explicitly checked.
3
Use sentinel errors for simple cases
4
Define error constants with errors.New() and check with errors.Is().
5
Use custom error types for complex cases
6
Create structs that implement the error interface and extract with errors.As().
7
Always check errors immediately
8
Handle errors at the point where they occur, not later.
The Cost of Ignoring Errors: If you don’t check err != nil, Go won’t stop you, but your program will behave unpredictably when failures occur. Silent failures are debugging nightmares.

Running the Examples

cd errorhandling
go run main.go
You’ll see both sentinel error checking and custom error extraction in action.

Next Steps

Build docs developers (and LLMs) love