ProgrammingGo Developer

What are the peculiarities of error handling in Go, how to properly create and handle errors, and how to implement custom error types?

Pass interviews with Hintsage AI assistant

Answer

Go was designed from the beginning around explicit error returns rather than exceptions. This allows for predictable code development and avoids the hidden traps associated with try/catch constructs in other languages (e.g., Java or C++). An error in Go is an interface that implements the Error() method, allowing for the creation of both simple and composite/wrapped errors with context.

Problems arise from improper error handling (e.g., if they are ignored via _ or not wrapped with additional debugging information) or creating "magic" errors that do not conform to the error interface. It is also important to return errors from public functions and check them in every call.

The solution is to use standard approaches:

  • Always return the error as the last value of the function.
  • Use errors.New, fmt.Errorf to create errors.
  • For user-defined errors, create your types that implement Error().
  • For complex cases, apply error wrappers (errors.Wrap) to retain context.

Code example:

import ( "errors" "fmt" ) type NotFoundError struct { Resource string } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s not found", e.Resource) } func GetUser(id int) (string, error) { if id != 1 { return "", &NotFoundError{"User"} } return "Steve", nil } func main() { user, err := GetUser(2) if err != nil { if nfe, ok := err.(*NotFoundError); ok { fmt.Println(nfe.Resource, "problem") } else { fmt.Println("error:", err) } } fmt.Println("user:", user) }

Key features:

  • Errors are always explicitly returned, usually as the last argument of the function.
  • For complex errors, you can define your types (struct) that implement the error interface.
  • For wrapping errors and preserving the stack trace, you can use the "errors" package (Go 1.13+ supports errors.Is and errors.As for working with error chains).

Trick Questions.

Can you compare errors using == when handling them, or should you use special methods?

It is better to always use errors.Is() for comparison with wrapped errors; otherwise, comparison via == may not work when errors are wrapped.

if errors.Is(err, os.ErrNotExist) { // handling file not found }

Is it mandatory to implement a struct type for a custom error, or is it sufficient to use errors.New("...")?

If you only need the error text, errors.New() is sufficient. If preserving context (e.g., resource name) is important, it is better to define a struct with the Error() method.

How should you return an error in the case of a successful function execution?

In the case of success, always return a nil error.

return result, nil

Common Errors and Anti-patterns

  • Ignoring returned errors (via _ or skipping handling)
  • Comparing errors only with ==, despite nested errors
  • Hardcoded strings instead of errors
  • Lack of additional context in errors

Real-life Example

Negative Case

A developer writes a function returning an error without explanations (just errors.New("fail")). When analyzing logs, it is impossible to determine the cause.

Pros:

  • Speed of implementation

Cons:

  • Uninformative errors
  • Debugging difficulties

Positive Case

A developer defines a custom error type NotFoundError and returns it with detailed description.

Pros:

  • Convenience of log filtering
  • Ease of finding and handling errors

Cons:

  • Need to describe additional structures