Skip to content

Error Handling

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.

TypeScript
// TypeScript: exceptions
async 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
// Go: errors as return values
func 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
}
TypeScript
// TypeScript
throw new Error("something went wrong");
class NotFoundError extends Error {
constructor(public id: string) {
super(`item ${id} not found`);
}
}
Go
// Go
import "errors"
import "fmt"
err := errors.New("something went wrong")
// Sentinel error (comparable with errors.Is)
var ErrNotFound = errors.New("not found")
// Custom error type
type NotFoundError struct{ ID string }
func (e *NotFoundError) Error() string {
return fmt.Sprintf("item %s not found", e.ID)
}
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)
}
}
How does Go signal a function failure to its caller?
What does fmt.Errorf("context: %w", err) do?
errors.Is(err, target) returns true when:
When should you use panic in Go?