ProgrammingBackend Developer

Explain how error handling works through the error wrapping package in Go: what is error wrapping, why is it important, and how to properly implement error wrapping?

Pass interviews with Hintsage AI assistant

Answer.

Background:

Before Go 1.13, an error was simply an interface. To provide additional context for errors, developers often created their own types, but there was no structured mechanism for wrapping errors like in modern Java or C#. With the introduction of Go 1.13, a standard method for error wrapping was introduced through fmt.Errorf and for identifying root causes (errors.Is/errors.As).

Problem:

In complex applications where errors can occur at various levels of the stack, it is crucial not to lose context when returning an error from deep inside the code. Otherwise, debugging becomes difficult and it’s impossible to tell where along the chain the failure occurred.

Solution:

Go allows wrapping errors in new error objects that contain the cause. This is done using %w in fmt.Errorf, and to check underlying causes — errors.Is or errors.As from the errors package.

Code Example:

import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func getData() error { return fmt.Errorf("service database: %w", ErrNotFound) } func main() { err := getData() if errors.Is(err, ErrNotFound) { fmt.Println("Detected cause: not found") } else { fmt.Println("Other error:", err) } }

Key Features:

  • Using %w for wrapping errors via fmt.Errorf.
  • Detecting nested causes of errors through errors.Is and errors.As.
  • Compatibility with custom error types when wrapping and unwrapping.

Trick Questions.

What formatting verb is needed for wrapping errors via fmt.Errorf?

You should use %w, not %v — only %w supports unwrapping.

Code Example:

fmt.Errorf("error: %w", err)

Can you manually create error chains without fmt.Errorf and still detect them using errors.Is?

No, you need to implement the Unwrap interface; otherwise, the standard functions will not unwrap the chain.

Example of the interface:

type wrappedError struct { msg string err error } func (w wrappedError) Error() string { return w.msg + ": " + w.err.Error() } func (w wrappedError) Unwrap() error { return w.err }

Do errors return nil values after wrapping if the underlying error was nil?

No, if the underlying error is nil, the wrapped error is also nil only if used correctly. But if you manually create a wrapper struct where the error field is nil, it will not be nil. This often leads to confusion during checks.

Common Errors and Anti-patterns

  • Not using %w, but only %v — losing the ability to trace the chain.
  • Not implementing Unwrap() for your error structures.
  • Not performing checks via errors.Is/As, but only comparing errors directly.
  • Creating new errors without causes (loss of context).

Real-life Example

Negative Case

In the application, a new error was simply returned with text at each level:

return errors.New("database write error")

Pros:

  • Simple and fast.

Cons:

  • Impossible to distinguish that the error is actually "not found" from deep inside, potentially breaking business logic at the top.
  • Loss of stack information and error source.

Positive Case

Error wrapping was used with %w and analysis through errors.Is:

if errors.Is(err, ErrNotFound) { return fmt.Errorf("service level error: %w", err) }

Pros:

  • Correctly identifies the cause at any level.
  • Easier to debug, the original context is always visible.

Cons:

  • Requires understanding of how wrapping works and skillful error writing.