Error Handling in Go: try/catch and Beyond — Advanced Level

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Error Handling in Go: try/catch and Beyond — Advanced Level

\n\n

Unlike many modern languages (Java, Python, JavaScript), Go does not have traditional try/catch/finally constructs. Developers coming from other ecosystems often perceive this as a drawback. However, Go's philosophy offers a more explicit and predictable way to work with errors. In this article, we will explore advanced error handling techniques in Go: from basic principles to creating flexible systems with panic/recover, error wrapping, and custom types.

\n\n

1. Why is there no try/catch in Go?

\n\n

The main reason is code simplicity and readability. In Go, an error is simply a value (error) that a function returns as its last result. The developer explicitly checks this value, rather than relying on a hidden exception flow. This makes the program's execution flow obvious.

\n\n

The standard pattern looks like this:

\n\n
package main\n\nimport (\n    "fmt"\n    "os"\n)\n\nfunc readFile(name string) ([]byte, error) {\n    data, err := os.ReadFile(name)\n    if err != nil {\n        return nil, fmt.Errorf("failed to read file %s: %w", name, err)\n    }\n    return data, nil\n}\n\nfunc main() {\n    data, err := readFile("config.json")\n    if err != nil {\n        fmt.Println("Error:", err)\n        return\n    }\n    fmt.Println("Data:", string(data))\n}
\n\n

Here, every potential error is handled explicitly. This is not "noise," but a guarantee that the programmer has considered the possibility of failure.

\n\n

2. Advanced Techniques: Wrapping and Error Chains

\n\n

Go 1.13 introduced support for error wrapping via %w in fmt.Errorf. This allows creating error chains while preserving the original cause. To unwrap such chains, the errors.Is and errors.As functions are used.

\n\n

Example of wrapping and checking:

\n\n
package main\n\nimport (\n    "errors"\n    "fmt"\n)\n\nvar ErrNotFound = errors.New("record not found")\n\nfunc findUser(id int) (string, error) {\n    if id != 42 {\n        return "", fmt.Errorf("searching for user %d: %w", id, ErrNotFound)\n    }\n    return "Alice", nil\n}\n\nfunc main() {\n    _, err := findUser(100)\n    if errors.Is(err, ErrNotFound) {\n        fmt.Println("Error: record missing. Details:", err)\n    } else {\n        fmt.Println("Unknown error:", err)\n    }\n}
\n\n

Using errors.As for typed errors:

\n\n
package main\n\nimport (\n    "errors"\n    "fmt"\n)\n\ntype ValidationError struct {\n    Field string\n    Msg   string\n}\n\nfunc (e *ValidationError) Error() string {\n    return fmt.Sprintf("validation error for field %s: %s", e.Field, e.Msg)\n}\n\nfunc validateAge(age int) error {\n    if age < 0 {\n        return &ValidationError{Field: "age", Msg: "age cannot be negative"}\n    }\n    return nil\n}\n\nfunc main() {\n    err := validateAge(-5)\n    var ve *ValidationError\n    if errors.As(err, &ve) {\n        fmt.Printf("Field: %s, Message: %s\\n", ve.Field, ve.Msg)\n    } else {\n        fmt.Println("Unknown error:", err)\n    }\n}
\n\n

3. Panic and Recover: Emergency Recovery

\n\n

Although panic in Go is an analog of an exception, it should only be used for truly exceptional situations (e.g., programmer logic error, division by zero, index out of bounds). recover allows you to catch a panic and resume execution.

\n\n

Important: recover only works inside a deferred function (defer). A typical scenario is middleware in an HTTP server or protecting a goroutine.

\n\n

Example of a protected goroutine launch:

\n\n
package main\n\nimport (\n    "fmt"\n    "time"\n)\n\nfunc safeGoroutine(fn func()) {\n    go func() {\n                

Blogs

Book Recommendations