Go Custom Error Types
Internal
Overview
The values of any type that implements the error
interface can be used as errors. An improvement of error types over error values is their ability to provide more context: instead of a string that contains a human-readable error message, the error instance may carry additional information like (for example) line number, user IDs, etc., which can be used programmatically by error handling code. Error types may even wrap an underlying error in the new type to provide more context. See os.PathError
for such an example. As mentioned in the Error Basics section, if the error context elements need to be processed programmatically as part of the program logic, use a custom type instead of parsing the error message.
Custom error types can be used to wrap other errors and mark them as of a specific type.
As long as the specific type is not used in error handling, in type assertion if
and switch
statements, custom error types values can be used as any other generic error values and do not introduce additional complications.
However, the problems start when error types must be made public, so the caller can use a type assertion or a type switch. If your code implements an interface whose contract requires a specific error type, all implementors of that interface need to depend on the package that defines the error type. This knowledge of a package's types creates a strong coupling with the caller, making for a brittle API. This is a reason to avoid error types, at least to avoid making them part of the public API.
Naming
Error types should be of the form SomethingError:
type SomethingError struct {
...
}
Type Aliases as Error Types
If we need to propagate just one specific value with an error, so we can programmatically handle that value as part of subsequent error handling, aliasing the value's type and turning it into an error type by making it implement the error
interface is a pattern that can be used.
For example, if we want to recover an invalid value passed to a square root function sqrt()
function, we can alias float64
to NegaativeSqrtError
and have the type implement the error
interface:
type NegativeSqrtError float64
// Error makes NegativeSqrtError type implement the error interface
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of a negative number %g", float64(f))
}
func sqrt(arg float64) (float64, error) {
if arg < 0 {
return -1, NegativeSqrtError(arg)
}
return math.Sqrt(arg), nil
}
...
r, err := sqrt(-1)
if err != nil {
_, assertionTrue := err.(NegativeSqrtError)
if assertionTrue {
fmt.Printf("%v\n", err)
} else {
fmt.Println("other kind of error")
}
} else {
fmt.Println(r)
}
A type alias error does not need a constructor, it can be built with type casting:
...
return NegativeSqrtError(some_float)
Structs as Error Types
A struct becomes an error type if it implements the Error()
method of the error
interface. It makes sense to declare a struct
only if the context supposed to be carried by the error instance has more than one field. If only one filed is needed, a type alias will do.
type SyntaxError struct {
line int
column int
msg string
}
// Error makes SyntaxError type implement the error interface
func (e SyntaxError) Error() string {
return fmt.Sprintf("[%d, %d]: %s", e.line, e.column, e.msg)
}
func parse() error {
...
return SyntaxError{10, 10, "missing identifier"}
...
}
...
err := parse()
if _, assertionTrue := err.(SyntaxError); assertionTrue {
fmt.Printf("%s\n", err)
}
Wrapping Errors in Custom Error Types
A custom error types can be designed to wrap errors, the way os.PathError
does. For that, the struct should have an error
field, and the custom error type should implement Unwrap() error
method, so methods like errors.Is()
and errors.As()
work correctly:
type SyntaxError struct {
line int
column int
msg string
err error
}
// Error makes SyntaxError type implement the error interface
func (e SyntaxError) Error() string {
s := fmt.Sprintf("[%d, %d]: %s", e.line, e.column, e.msg)
if e.err != nil {
s += fmt.Sprintf(": %s", e.err)
}
return s
}
// Unwrap gives access to the wrapped error, so methods like errors.Is() and errors.As() work correctly
func (e SyntaxError) Unwrap() error {
return e.err
}
...
err := errors.New("some low-level error")
se := SyntaxError{10, 10, "missing identifier", err}
println(errors.Is(se, err)) // will print true