Error Handling in Go: try/catch and Beyond — Advanced Level
\n\nUnlike 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.
1. Why is there no try/catch in Go?
\n\nThe 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.
The standard pattern looks like this:
\n\npackage 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\nHere, every potential error is handled explicitly. This is not "noise," but a guarantee that the programmer has considered the possibility of failure.
\n\n2. Advanced Techniques: Wrapping and Error Chains
\n\nGo 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.
Example of wrapping and checking:
\n\npackage 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\nUsing errors.As for typed errors:
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\n3. Panic and Recover: Emergency Recovery
\n\nAlthough 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.
Important: recover only works inside a deferred function (defer). A typical scenario is middleware in an HTTP server or protecting a goroutine.
Example of a protected goroutine launch:
\n\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc safeGoroutine(fn func()) {\n go func() {\n