Error Handling
Error Handling
Introduction
In this lesson, you'll learn about error handling in Go. Coming from TypeScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In TypeScript, you're familiar with error handling.
Go has its own approach to error handling, which we'll explore step by step.
The Go Way
Let's see how Go handles this concept. Here's a typical example:
// Custom error type
type NotFoundError struct {
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("user %d not found", e.ID)
}
func getUser(id int) (*User, error) {
user, ok := db[id]
if !ok {
return nil, &NotFoundError{ID: id}
}
return &user, nil
}
user, err := getUser(42)
if err != nil {
var notFound *NotFoundError
if errors.As(err, ¬Found) {
fmt.Println("Not found:", notFound.ID)
}
}Comparing to TypeScript
Here's how you might have written similar code in TypeScript:
class NotFoundError extends Error {
constructor(public id: number) {
super(`User ${id} not found`);
}
}
async function getUser(id: number): Promise<User> {
const user = await db.find(id);
if (!user) throw new NotFoundError(id);
return user;
}
try {
const user = await getUser(42);
} catch (err) {
if (err instanceof NotFoundError) {
console.error("Not found:", err.id);
}
}You may be used to different syntax or behavior.
Go has no exceptions — errors are values returned as the last return value
You may be used to different syntax or behavior.
No try/catch — check err != nil after every call that can fail
You may be used to different syntax or behavior.
Custom errors implement the error interface (single method: Error() string)
You may be used to different syntax or behavior.
errors.As() unwraps error chains, like instanceof for errors
Step-by-Step Breakdown
1. Errors Are Values, Not Exceptions
Go has no exceptions. Functions return errors as the last return value. This forces explicit handling at every call site — no hidden control flow.
try {
const data = await riskyOperation();
} catch (e) {
handleError(e);
}data, err := riskyOperation()
if err != nil {
handleError(err)
return
}2. Custom Error Types
Any type that implements the error interface (Error() string method) is an error. This is structural typing applied to errors.
class ValidationError extends Error {
constructor(public field: string) {
super(`Invalid field: ${field}`);
}
}type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "invalid field: " + e.Field
}3. errors.As and errors.Is
errors.As() unwraps an error chain to find a specific type (like instanceof). errors.Is() checks if an error matches a specific sentinel value.
if (err instanceof NotFoundError) { ... }var notFound *NotFoundError
if errors.As(err, ¬Found) {
fmt.Println(notFound.ID)
}
// Check sentinel:
if errors.Is(err, ErrNotFound) { ... }4. fmt.Errorf and Error Wrapping
fmt.Errorf with %w wraps an error with context. This creates a chain that errors.As/Is can traverse — like cause chains in Java or error wrapping in some TS libraries.
throw new Error(`fetch failed: ${originalError.message}`);if err != nil {
return fmt.Errorf("getUser: %w", err) // wraps original
}
// Later, unwrap:
original := errors.Unwrap(wrappedErr)Common Mistakes
When coming from TypeScript, developers often make these mistakes:
- Go has no exceptions — errors are values returned as the last return value
- No try/catch — check err != nil after every call that can fail
- Custom errors implement the error interface (single method: Error() string)
Key Takeaways
- Go has no exceptions — errors are returned as values, forcing explicit handling
- Custom error types implement the error interface (Error() string)
- errors.As() checks type, errors.Is() checks value — replace instanceof/equality
- fmt.Errorf with %w wraps errors with context for error chains