Error Handling
Errors as values, not exceptions
Section titled “Errors as values, not exceptions”TypeScript (and JavaScript) use throw/try/catch for error handling — exceptions propagate up the call stack invisibly. Go takes a different approach: errors are just values. A function that can fail returns an error as its last return value. The caller decides what to do with it immediately.
This feels verbose at first. After a while you’ll appreciate that every failure path is explicit and traceable.
The basic pattern
Section titled “The basic pattern”// TypeScript: exceptionsasync function readConfig(path: string): Promise<Config> { try { const data = await fs.readFile(path, "utf-8"); return JSON.parse(data); } catch (err) { throw new Error(`failed to read config: ${err}`); }}// Go: errors as return valuesfunc readConfig(path string) (Config, error) { data, err := os.ReadFile(path) if err != nil { return Config{}, fmt.Errorf("failed to read config: %w", err) } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return Config{}, fmt.Errorf("failed to parse config: %w", err) } return cfg, nil}Creating errors
Section titled “Creating errors”// TypeScriptthrow new Error("something went wrong");
class NotFoundError extends Error { constructor(public id: string) { super(`item ${id} not found`); }}// Goimport "errors"import "fmt"
err := errors.New("something went wrong")
// Sentinel error (comparable with errors.Is)var ErrNotFound = errors.New("not found")
// Custom error typetype NotFoundError struct{ ID string }func (e *NotFoundError) Error() string { return fmt.Sprintf("item %s not found", e.ID)}Wrapping and unwrapping with %w
Section titled “Wrapping and unwrapping with %w”package main
import ( "errors" "fmt")
var ErrNotFound = errors.New("not found")
type ValidationError struct { Field string Message string}
func (e *ValidationError) Error() string { return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)}
func findUser(id int) error { if id <= 0 { return &ValidationError{Field: "id", Message: "must be positive"} } if id > 100 { return fmt.Errorf("findUser: %w", ErrNotFound) } return nil}
func main() { // errors.Is unwraps the chain err := findUser(999) if errors.Is(err, ErrNotFound) { fmt.Println("user not found (sentinel matched)") }
// errors.As extracts the concrete type err = findUser(-1) var ve *ValidationError if errors.As(err, &ve) { fmt.Printf("bad field: %s — %s\n", ve.Field, ve.Message) }}Loading Go runtime (first run only, ~8 MB)…