Bird
Raised Fist0
Goprogramming~15 mins

Custom error types in Go - Deep Dive

Choose your learning style10 modes available
Overview - Custom error types
What is it?
Custom error types in Go let you create your own error messages with extra details. Instead of just a simple text error, you can add information like error codes or context. This helps programs handle errors more clearly and specifically. It’s like making your own special error notes that tell exactly what went wrong.
Why it matters
Without custom error types, all errors look the same and only show simple messages. This makes it hard to fix problems or decide what to do next. Custom errors let programs understand different problems better, so they can respond correctly and keep running smoothly. It’s like having a detailed map instead of a vague direction.
Where it fits
Before learning custom error types, you should know basic Go errors and how to use the built-in error interface. After this, you can learn about error wrapping and handling patterns to build robust programs that recover from or log errors properly.
Mental Model
Core Idea
A custom error type is a special kind of error that carries extra information beyond a simple message, letting your program understand and react to errors more precisely.
Think of it like...
Imagine a mail system where normal letters are just plain envelopes with a message, but custom error types are like envelopes with colored labels and extra notes inside, so the mailroom knows exactly how to handle each letter.
┌───────────────┐
│   error       │
│  interface   ◄─────────────┐
└───────────────┘             │
       ▲                     │
       │                     │
┌───────────────┐      ┌───────────────┐
│ Basic error   │      │ Custom error  │
│ (string msg)  │      │ (extra fields)│
└───────────────┘      └───────────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding Go's error interface
🤔
Concept: Learn what the error interface is and how Go uses it to represent errors.
In Go, an error is any value that implements the error interface. This interface has one method: Error() string. For example: package main import "fmt" func main() { var err error = fmt.Errorf("simple error") fmt.Println(err.Error()) } This prints: simple error
Result
The program prints the error message string.
Understanding the error interface is key because all errors in Go must implement it, making errors flexible and consistent.
2
FoundationCreating a basic custom error type
🤔
Concept: Define a new struct type that implements the error interface with a custom message.
You can create your own error type by making a struct and adding an Error() method: package main import "fmt" type MyError struct { Msg string } func (e MyError) Error() string { return e.Msg } func main() { err := MyError{Msg: "something went wrong"} fmt.Println(err.Error()) } This prints: something went wrong
Result
The program prints the custom error message from MyError.
Knowing that any type with Error() string is an error lets you add fields to carry more info.
3
IntermediateAdding extra context to errors
🤔Before reading on: do you think adding fields like codes or timestamps to errors helps or just complicates error handling? Commit to your answer.
Concept: Enhance custom errors by including extra details like error codes or context fields.
You can add fields to your error struct to hold more information: package main import "fmt" type MyError struct { Code int Msg string } func (e MyError) Error() string { return fmt.Sprintf("Error %d: %s", e.Code, e.Msg) } func main() { err := MyError{Code: 404, Msg: "not found"} fmt.Println(err.Error()) } This prints: Error 404: not found
Result
The program prints a detailed error message with code and text.
Adding context fields lets your program decide what to do based on error details, not just text.
4
IntermediateUsing type assertions to handle errors
🤔Before reading on: do you think you can check error types directly or must you parse error messages? Commit to your answer.
Concept: Learn how to check if an error is a specific custom type using type assertions.
You can test if an error is your custom type and access its fields: package main import "fmt" type MyError struct { Code int Msg string } func (e MyError) Error() string { return fmt.Sprintf("Error %d: %s", e.Code, e.Msg) } func main() { var err error = MyError{Code: 500, Msg: "server error"} if e, ok := err.(MyError); ok { fmt.Println("Custom error code:", e.Code) } else { fmt.Println("Other error") } } This prints: Custom error code: 500
Result
The program detects the custom error type and prints its code.
Type assertions let you handle different error types differently, improving control flow.
5
IntermediateImplementing error wrapping with custom types
🤔Before reading on: do you think custom errors can wrap other errors to keep original info? Commit to your answer.
Concept: Custom errors can wrap other errors to keep original error details while adding context.
Go 1.13+ supports error wrapping with fmt.Errorf and errors.Unwrap: package main import ( "errors" "fmt" ) type MyError struct { Msg string Err error } func (e MyError) Error() string { return e.Msg + ": " + e.Err.Error() } func (e MyError) Unwrap() error { return e.Err } func main() { baseErr := errors.New("file not found") err := MyError{Msg: "read failed", Err: baseErr} fmt.Println(err.Error()) if errors.Is(err, baseErr) { fmt.Println("Detected base error") } } Output: read failed: file not found Detected base error
Result
The program prints the combined error message and detects the base error.
Wrapping preserves original errors, enabling layered error handling and better debugging.
6
AdvancedCustom errors in production: best practices
🤔Before reading on: do you think all errors should be custom types or only some? Commit to your answer.
Concept: Learn when and how to use custom errors effectively in real-world Go programs.
In production, use custom errors to: - Add machine-readable codes for error handling - Wrap errors to keep full context - Implement helper functions to create and check errors Example pattern: package errors type ErrorCode int const ( ErrNotFound ErrorCode = iota ErrPermission ) type MyError struct { Code ErrorCode Msg string Err error } func (e MyError) Error() string { return e.Msg } func (e MyError) Unwrap() error { return e.Err } // Usage in app: // if err != nil { // if e, ok := err.(MyError); ok && e.Code == ErrNotFound { // // handle not found // } // } This approach helps maintain clear error handling and debugging.
Result
Custom errors improve clarity and control in large programs.
Knowing when to use custom errors avoids overcomplicating code and keeps error handling maintainable.
7
ExpertPerformance and pitfalls of custom error types
🤔Before reading on: do you think custom errors always improve performance or can they add overhead? Commit to your answer.
Concept: Understand the internal cost and subtle bugs that can happen with custom error types.
Custom errors add struct allocations and method calls, which can affect performance in tight loops. Also, pointer vs value receiver differences can cause bugs: // Pointer receiver example func (e *MyError) Error() string { ... } // Value receiver example func (e MyError) Error() string { ... } If you mix pointer and value errors, type assertions may fail unexpectedly. Also, forgetting to implement Unwrap() breaks error unwrapping. Profiling and careful design help avoid these issues.
Result
Custom errors can slow programs if overused and cause subtle bugs if not designed carefully.
Understanding internals prevents common bugs and performance hits in error handling.
Under the Hood
Go treats errors as interfaces with one method Error() string. When you create a custom error type, you define a struct with fields and implement this method. At runtime, Go stores the concrete type and value behind the error interface. When you call Error(), Go calls your method. For error wrapping, Go 1.13+ uses the Unwrap() method to access the original error, enabling functions like errors.Is and errors.As to work by walking the chain.
Why designed this way?
Go’s error interface is minimal to keep error handling simple and flexible. Custom types let programmers add details without changing the language. The wrapping design was added later to improve error context without breaking old code. This design balances simplicity, extensibility, and backward compatibility.
┌───────────────┐
│  error iface  │
│  (Error())   ◄─────────────┐
└───────────────┘             │
       ▲                     │
       │                     │
┌───────────────┐      ┌───────────────┐
│ Custom error  │      │ Wrapped error │
│ struct +      │      │ struct +      │
│ Error()       │      │ Error() +     │
│ Unwrap()      │      │ Unwrap()      │
└───────────────┘      └───────────────┘
       │                     ▲
       └─────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Do you think all errors must be strings only? Commit yes or no.
Common Belief:Errors are just strings and adding fields is unnecessary complexity.
Tap to reveal reality
Reality:Errors are interfaces and can be any type with Error() string, allowing rich data inside.
Why it matters:Treating errors as plain strings limits error handling and makes programs fragile.
Quick: Can you always compare errors with == to check their type? Commit yes or no.
Common Belief:You can compare errors directly with == to check their type or value.
Tap to reveal reality
Reality:Errors are interfaces; comparing with == only works if they are the same object. Use errors.Is or type assertions instead.
Why it matters:Wrong comparisons cause bugs where errors are not detected or handled properly.
Quick: Does wrapping an error change its original message? Commit yes or no.
Common Belief:Wrapping an error replaces the original error message with a new one.
Tap to reveal reality
Reality:Wrapping adds context but preserves the original error, accessible via Unwrap().
Why it matters:Losing original error info makes debugging harder and can hide root causes.
Quick: Is it safe to use value receivers for custom error methods always? Commit yes or no.
Common Belief:Using value receivers for Error() is always safe and recommended.
Tap to reveal reality
Reality:Using value receivers can cause type assertion failures if errors are passed as pointers elsewhere.
Why it matters:Inconsistent receiver types cause subtle bugs in error handling and type checks.
Expert Zone
1
Custom error types can implement multiple interfaces to support rich behaviors like temporary errors or timeout detection.
2
The choice between pointer and value receivers for Error() affects error comparisons and should be consistent across your codebase.
3
Error wrapping chains can become long; designing clear error hierarchies and helper functions improves maintainability.
When NOT to use
Avoid custom error types for trivial errors where a simple error string suffices. For complex error handling, consider using structured logging or dedicated error handling libraries that provide more features.
Production Patterns
In production, custom errors are used with error codes for API responses, wrapped to preserve context, and checked with errors.Is or errors.As. Libraries often provide helper functions to create and classify errors, enabling clean and maintainable error handling.
Connections
Interfaces in Go
Custom error types build on the error interface concept.
Understanding interfaces deeply helps grasp how custom errors work and how to design flexible error types.
Exception handling in other languages
Custom error types in Go serve a similar role to exceptions with types in languages like Java or Python.
Knowing how exceptions carry type info helps appreciate why Go uses interfaces and custom types for errors.
Medical diagnosis process
Like doctors use detailed symptoms (error fields) to diagnose illness (error cause), custom errors carry detailed info to diagnose program issues.
This cross-domain link shows how adding context improves problem-solving accuracy.
Common Pitfalls
#1Using plain strings for all errors, losing context.
Wrong approach:return errors.New("failed to open file")
Correct approach:return MyError{Code: 1, Msg: "failed to open file"}
Root cause:Not realizing that custom types can carry more info than plain strings.
#2Mixing pointer and value receivers causing type assertion failures.
Wrong approach:func (e MyError) Error() string { return e.Msg } // but passing &MyError{} as error
Correct approach:func (e *MyError) Error() string { return e.Msg } // consistent pointer receiver
Root cause:Confusion about method receivers and interface implementation.
#3Not implementing Unwrap() when wrapping errors, breaking errors.Is.
Wrong approach:type MyError struct { Msg string; Err error } func (e MyError) Error() string { return e.Msg + ": " + e.Err.Error() }
Correct approach:func (e MyError) Unwrap() error { return e.Err }
Root cause:Forgetting that error unwrapping requires the Unwrap() method.
Key Takeaways
Custom error types in Go let you add meaningful details to errors beyond simple messages.
Any type with an Error() string method satisfies the error interface, enabling flexible error design.
Type assertions and errors.Is help you detect and handle specific custom errors safely.
Error wrapping preserves original errors and context, improving debugging and control flow.
Consistent design of custom errors avoids subtle bugs and keeps your programs maintainable.

Practice

(1/5)
1. What is the main purpose of creating a custom error type in Go?
easy
A. To add extra information and context to errors
B. To make errors print in uppercase letters
C. To automatically fix errors in the code
D. To prevent errors from being returned

Solution

  1. Step 1: Understand the role of custom error types

    Custom error types allow programmers to include more details about what caused the error, beyond just a simple message.
  2. Step 2: Compare options with this understanding

    Only To add extra information and context to errors correctly describes adding extra information and context. Other options describe unrelated or incorrect behaviors.
  3. Final Answer:

    To add extra information and context to errors -> Option A
  4. Quick Check:

    Custom error types add context = B [OK]
Hint: Custom errors add details, not fix or hide errors [OK]
Common Mistakes:
  • Thinking custom errors fix problems automatically
  • Believing custom errors change error printing style
  • Assuming custom errors stop errors from returning
2. Which of the following is the correct way to define a custom error type in Go?
easy
A. type MyError string func (e MyError) Print() { fmt.Println(e) }
B. func MyError() string { return "error" }
C. type MyError struct { msg string } func (e MyError) Error() string { return e.msg }
D. var MyError = errors.New("error")

Solution

  1. Step 1: Recall how to create custom error types

    In Go, a custom error type is a struct that implements the Error() string method.
  2. Step 2: Check each option

    type MyError struct { msg string } func (e MyError) Error() string { return e.msg } defines a struct and implements Error() correctly. Options A, B, and D do not implement the Error() method properly or define a struct.
  3. Final Answer:

    type MyError struct { msg string } func (e MyError) Error() string { return e.msg } -> Option C
  4. Quick Check:

    Custom error = struct + Error() method [OK]
Hint: Custom error needs struct + Error() string method [OK]
Common Mistakes:
  • Defining error as a function without Error() method
  • Using string type without Error() method
  • Using errors.New instead of custom struct
3. What will be the output of this Go code?
package main
import (
  "errors"
  "fmt"
)
type MyError struct {
  Code int
}
func (e MyError) Error() string {
  return fmt.Sprintf("Error code: %d", e.Code)
}
func main() {
  var err error = MyError{Code: 404}
  if errors.As(err, &MyError{}) {
    fmt.Println("Custom error detected")
  } else {
    fmt.Println("Unknown error")
  }
}
medium
A. Unknown error
B. Custom error detected
C. Compilation error
D. Runtime panic

Solution

  1. Step 1: Understand errors.As usage

    errors.As(err, targetPtr) returns true if err matches the type pointed to by targetPtr.
  2. Step 2: Analyze the code's errors.As call

    &MyError{} is a pointer to type MyError. err is MyError{Code: 404}, which matches MyError exactly. errors.As succeeds and prints "Custom error detected".
  3. Final Answer:

    Custom error detected -> Option B
  4. Quick Check:

    errors.As(MyError, &MyError{}) matches [OK]
Hint: errors.As works with &MyError{} (pointer to literal) [OK]
Common Mistakes:
  • Passing &MyError{} instead of pointer to variable
  • Assuming errors.As works with value types
  • Confusing errors.Is with errors.As
4. Identify the error in this Go code snippet that tries to use a custom error type:
type MyError struct {
  Msg string
}
func (e *MyError) Error() string {
  return e.Msg
}

func doSomething() error {
  return MyError{Msg: "fail"}
}
medium
A. MyError should not have a pointer receiver for Error()
B. Error() method must return an int, not string
C. MyError struct must embed error interface
D. doSomething returns MyError value but Error() has pointer receiver

Solution

  1. Step 1: Check method receiver and returned type

    Error() is defined with pointer receiver (*MyError), but doSomething returns a value (MyError), not a pointer.
  2. Step 2: Understand interface satisfaction rules

    Because Error() has pointer receiver, only *MyError implements error interface, not MyError value. Returning MyError value causes a type mismatch.
  3. Final Answer:

    doSomething returns MyError value but Error() has pointer receiver -> Option D
  4. Quick Check:

    Pointer receiver means only pointer type implements error [OK]
Hint: Pointer receiver means return pointer, not value [OK]
Common Mistakes:
  • Returning value when method has pointer receiver
  • Thinking struct must embed error interface
  • Expecting Error() to return int
5. You want to create a custom error type that holds an error code and message, and you want to check for this error type using errors.As. Which of the following code snippets correctly implements this pattern?
hard
A. type CodeError struct { Code int; Msg string } func (e *CodeError) Error() string { return e.Msg } func main() { var err error = &CodeError{Code: 123, Msg: "fail"} var target *CodeError if errors.As(err, &target) { fmt.Println(target.Code) } }
B. type CodeError struct { Code int; Msg string } func (e CodeError) Error() string { return e.Msg } func main() { var err error = CodeError{Code: 123, Msg: "fail"} var target *CodeError if errors.As(err, &target) { fmt.Println(target.Code) } }
C. type CodeError struct { Code int; Msg string } func (e CodeError) Error() string { return e.Msg } func main() { var err error = &CodeError{Code: 123, Msg: "fail"} var target CodeError if errors.As(err, &target) { fmt.Println(target.Code) } }
D. type CodeError struct { Code int; Msg string } func (e *CodeError) Error() string { return e.Msg } func main() { var err error = CodeError{Code: 123, Msg: "fail"} var target *CodeError if errors.As(err, &target) { fmt.Println(target.Code) } }

Solution

  1. Step 1: Check receiver type and error assignment

    type CodeError struct { Code int; Msg string } func (e *CodeError) Error() string { return e.Msg } func main() { var err error = &CodeError{Code: 123, Msg: "fail"} var target *CodeError if errors.As(err, &target) { fmt.Println(target.Code) } } uses pointer receiver for Error() and assigns &CodeError{} to err, which matches the pointer receiver.
  2. Step 2: Check errors.As usage

    type CodeError struct { Code int; Msg string } func (e *CodeError) Error() string { return e.Msg } func main() { var err error = &CodeError{Code: 123, Msg: "fail"} var target *CodeError if errors.As(err, &target) { fmt.Println(target.Code) } } passes &target where target is *CodeError, which is correct usage for errors.As to detect the custom error type.
  3. Final Answer:

    custom error with pointer receiver and errors.As check -> Option A
  4. Quick Check:

    Pointer receiver + pointer error + errors.As pointer variable = A [OK]
Hint: Match pointer receiver with pointer error and pointer target in errors.As [OK]
Common Mistakes:
  • Using value receiver but returning pointer or vice versa
  • Passing non-pointer target to errors.As
  • Assigning value error but using pointer receiver
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard