---
# === IDENTITY ===
id: software/patterns/error-handling-strategies/2026
canonical_question: "What are the best error handling strategies by language?"
aliases:
  - "error handling best practices"
  - "exception handling patterns"
  - "Result vs exceptions"
  - "Go error handling idioms"
  - "Rust Result Option error handling"
  - "Python exception hierarchy"
  - "TypeScript async error handling"
  - "when to throw vs return errors"
entity_type: software_reference
domain: software > patterns > error_handling_strategies
region: global
jurisdiction: global
temporal_scope: 2015-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.92
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: null
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Never swallow errors silently -- every caught error must be logged, propagated, or explicitly documented as intentionally ignored"
  - "Match the error handling idiom to the language -- do not use try/catch in Go or return error codes in Python"
  - "Async errors require different handling than sync errors -- unhandled Promise rejections crash Node.js 15+ processes"
  - "Custom error types must preserve the original error chain for debugging -- always wrap, never replace"
  - "Error messages must never expose internal state, stack traces, or credentials to end users"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need a resilience pattern for transient failures (retries, circuit breakers)"
    use_instead: "software/patterns/retry-exponential-backoff/2026 or software/patterns/circuit-breaker/2026"
  - condition: "Need HTTP status code mapping or API error response format"
    use_instead: "software/patterns/api-error-responses/2026"
  - condition: "Need logging/observability setup rather than error handling logic"
    use_instead: "software/system-design/logging-monitoring/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "language"
    question: "Which programming language are you using?"
    type: choice
    options: ["Python", "TypeScript/JavaScript", "Go", "Rust", "Java", "C#"]
  - key: "error_context"
    question: "What type of errors are you handling?"
    type: choice
    options: ["I/O and network errors", "validation errors", "business logic errors", "unrecoverable/fatal errors"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/error-handling-strategies/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/patterns/circuit-breaker/2026"
      label: "Circuit Breaker Pattern"
    - id: "software/patterns/retry-exponential-backoff/2026"
      label: "Retry with Exponential Backoff"
    - id: "software/patterns/middleware-pipeline/2026"
      label: "Middleware Pipeline Pattern"
  solves:
    - id: "software/debugging/nodejs-unhandled-promises/2026"
      label: "Node.js Unhandled Promise Rejections"
    - id: "software/debugging/python-importerror/2026"
      label: "Python ImportError Debugging"
  often_confused_with:
    - id: "software/patterns/state-machine-implementation/2026"
      label: "State Machine Implementation -- models state transitions, not error flows"
    - id: "software/patterns/retry-exponential-backoff/2026"
      label: "Retry with Exponential Backoff -- resilience strategy, not error handling logic"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "Error Handling - The Rust Programming Language"
    author: Rust Project
    url: https://doc.rust-lang.org/book/ch09-00-error-handling.html
    type: official_docs
    published: 2024-11-01
    reliability: authoritative
  - id: src2
    title: "Working with Errors in Go 1.13"
    author: The Go Blog
    url: https://go.dev/blog/go1.13-errors
    type: official_docs
    published: 2019-10-17
    reliability: authoritative
  - id: src3
    title: "Built-in Exceptions -- Python 3 Documentation"
    author: Python Software Foundation
    url: https://docs.python.org/3/library/exceptions.html
    type: official_docs
    published: 2024-10-01
    reliability: authoritative
  - id: src4
    title: "Error handling patterns"
    author: Andrea Bergia
    url: https://andreabergia.com/blog/2023/05/error-handling-patterns/
    type: technical_blog
    published: 2023-05-15
    reliability: high
  - id: src5
    title: "The Ultimate Guide to Error Handling in Python"
    author: Miguel Grinberg
    url: https://blog.miguelgrinberg.com/post/the-ultimate-guide-to-error-handling-in-python
    type: technical_blog
    published: 2024-03-12
    reliability: high
  - id: src6
    title: "Don't just check errors, handle them gracefully"
    author: Dave Cheney
    url: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
    type: technical_blog
    published: 2016-04-27
    reliability: high
  - id: src7
    title: "Error handling with async/await and promises"
    author: CatchJS
    url: https://catchjs.com/Docs/AsyncAwait
    type: technical_blog
    published: 2023-01-10
    reliability: moderate_high
---

# Error Handling Strategies by Language: Exceptions, Result Types, and Error Values

## TL;DR

- **Bottom line**: Use language-idiomatic error handling -- Result/Either types for recoverable errors in Rust/Go, exceptions for exceptional conditions in Python/Java/C#, and structured try/catch with typed errors in TypeScript.
- **Key tool/command**: `Result<T, E>` (Rust), `if err != nil` (Go), `try/except` (Python), `try/catch` with custom Error classes (TypeScript)
- **Watch out for**: Swallowing errors silently -- the #1 cause of hard-to-debug production failures across all languages.
- **Works with**: Python 3.8+, TypeScript 4.0+/Node.js 15+, Go 1.13+ (error wrapping), Rust 1.0+, Java 8+, C# 6+.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never swallow errors silently -- every caught error must be logged, propagated, or explicitly documented as intentionally ignored [src4]
- Match the error handling idiom to the language -- using Go-style error returns in Python or try/catch in Go produces unidiomatic, fragile code [src6]
- Async errors require different handling than sync errors -- unhandled Promise rejections crash Node.js 15+ processes by default [src7]
- Custom error types must preserve the original error chain for debugging -- always wrap, never replace [src2]
- Error messages exposed to end users must never contain stack traces, internal paths, or credentials [src5]

## Quick Reference

| Language | Mechanism | Recoverable Errors | Fatal Errors | Custom Errors | Best Practice |
|---|---|---|---|---|---|
| Python | `try`/`except` | Catch specific exceptions | `sys.exit()`, unhandled exceptions | Subclass `Exception` | Catch narrow, raise with context (`from e`) |
| TypeScript/JS | `try`/`catch` + `Promise` | `catch` block, `.catch()` | `process.exit()`, unhandled rejection | Extend `Error` class | Use `cause` property (ES2022), typed error guards |
| Go | Error values (`error` interface) | `if err != nil` return | `panic()`/`log.Fatal()` | Implement `error` interface | Wrap with `fmt.Errorf("%w", err)`, use `errors.Is`/`As` |
| Rust | `Result<T, E>` / `Option<T>` | `match`, `?` operator | `panic!()`, `.unwrap()` | Implement `std::error::Error` | Use `thiserror` (libraries) / `anyhow` (applications) |
| Java | Checked + unchecked exceptions | `try`/`catch` | `System.exit()`, `Error` subclasses | Extend `Exception` or `RuntimeException` | Prefer unchecked for recoverable; checked for cross-boundary |
| C# | Exceptions (all unchecked) | `try`/`catch`/`finally` | `Environment.FailFast()` | Extend `Exception` | Use exception filters (`when`), avoid `catch (Exception)` |

## Decision Tree

> Full script: [decision-tree.txt](scripts/decision-tree.txt) (26 lines)

```
START
+-- Is the error recoverable?
|   +-- YES
|   |   +-- Language uses Result types (Rust, Go)?
|   |   |   +-- Rust --> Return Result<T, E>, caller uses ? or match
# ... (see full script)
```

## Step-by-Step Guide

### 1. Define your error hierarchy

Create a base error type for your domain, then specific subtypes for each failure mode. This enables callers to catch broad or narrow categories as needed. [src3]

```python
# Python: error hierarchy
class AppError(Exception):
    """Base error for the application."""
    def __init__(self, message: str, code: str, cause: Exception | None = None):
        super().__init__(message)
        self.code = code
        self.__cause__ = cause

class ValidationError(AppError):
    """Input validation failed."""
    def __init__(self, field: str, message: str):
        super().__init__(message, code="VALIDATION_ERROR")
        self.field = field

class NotFoundError(AppError):
    """Requested resource does not exist."""
    def __init__(self, resource: str, identifier: str):
        super().__init__(f"{resource} '{identifier}' not found", code="NOT_FOUND")
```

**Verify**: `raise ValidationError("email", "invalid format")` --> should display `ValidationError: invalid format`

### 2. Implement error wrapping to preserve context

When catching and re-raising errors, wrap the original to maintain the full chain. [src2] [src6]

```go
// Go: error wrapping with fmt.Errorf %w
func fetchUser(id string) (*User, error) {
    row, err := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
    if err != nil {
        return nil, fmt.Errorf("fetchUser(%s): %w", id, err)
    }
    // ... scan row
    return &user, nil
}

// Caller: check wrapped errors
if errors.Is(err, sql.ErrNoRows) {
    // handle not found
}
```

**Verify**: `fmt.Println(errors.Is(wrappedErr, sql.ErrNoRows))` --> `true`

### 3. Set up global error handlers

Catch unhandled errors at the application boundary to prevent crashes and ensure logging. [src7]

```typescript
// TypeScript/Node.js: global handlers
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
  // Log to error tracking service (Sentry, Datadog, etc.)
  process.exit(1);
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught exception:', error);
  // Graceful shutdown: close DB connections, flush logs
  process.exit(1);
});
```

**Verify**: Trigger an unhandled rejection with `Promise.reject(new Error('test'))` --> should log and exit

### 4. Use typed error discrimination at catch sites

Instead of catching all errors and guessing, use type guards or pattern matching to handle specific error types. [src1]

```rust
// Rust: pattern matching on Result
use std::fs;
use std::io;

fn read_config(path: &str) -> Result<String, AppError> {
    fs::read_to_string(path).map_err(|e| match e.kind() {
        io::ErrorKind::NotFound => AppError::ConfigMissing(path.to_string()),
        io::ErrorKind::PermissionDenied => AppError::PermissionDenied(path.to_string()),
        _ => AppError::Io(e),
    })
}
```

**Verify**: Call with non-existent path --> should return `Err(AppError::ConfigMissing(...))`

## Code Examples

### Python: Structured Exception Handling with Context

```python
# Input:  HTTP request to external API
# Output: Parsed response or domain-specific error

import httpx  # httpx>=0.27

class ApiError(Exception):
    def __init__(self, status: int, body: str, cause: Exception | None = None):
        super().__init__(f"API returned {status}: {body}")
        self.status = status
        self.__cause__ = cause

def fetch_data(url: str) -> dict:
    try:
        resp = httpx.get(url, timeout=10.0)
        resp.raise_for_status()
        return resp.json()
    except httpx.TimeoutException as e:
        raise ApiError(0, "request timed out", cause=e) from e
    except httpx.HTTPStatusError as e:
        raise ApiError(e.response.status_code, e.response.text, cause=e) from e
```

### TypeScript: Async Error Handling with Result Pattern

```typescript
// Input:  Database query parameters
// Output: Result tuple [data, error] -- never throws

type Result<T> = [T, null] | [null, Error];

async function findUser(id: string): Promise<Result<User>> {
  try {
    const user = await db.query<User>('SELECT * FROM users WHERE id = $1', [id]);
    if (!user) return [null, new NotFoundError(`User ${id}`)];
    return [user, null];
  } catch (err) {
    return [null, err instanceof Error ? err : new Error(String(err))];
  }
}

// Usage: no try/catch needed at call site
const [user, err] = await findUser('123');
if (err) {
  logger.error('Failed to find user', { error: err, userId: '123' });
  return res.status(err instanceof NotFoundError ? 404 : 500).json({ error: err.message });
}
```

### Go: Sentinel Errors and errors.Is/As

> Full script: [go-sentinel-errors-and-errors-is-as.go](scripts/go-sentinel-errors-and-errors-is-as.go) (27 lines)

```go
// Input:  File path to read
// Output: File contents or wrapped error
package main
import (
    "errors"
# ... (see full script)
```

### Rust: thiserror for Library Errors

```rust
// Input:  Config file path
// Output: Parsed config or typed error

use thiserror::Error; // thiserror = "2"

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("config file not found: {path}")]
    NotFound { path: String },
    #[error("invalid config format: {0}")]
    ParseError(#[from] serde_json::Error),
    #[error("I/O error reading config")]
    Io(#[from] std::io::Error),
}

pub fn load_config(path: &str) -> Result<Config, ConfigError> {
    let contents = std::fs::read_to_string(path)
        .map_err(|e| match e.kind() {
            std::io::ErrorKind::NotFound => ConfigError::NotFound { path: path.into() },
            _ => ConfigError::Io(e),
        })?;
    let config: Config = serde_json::from_str(&contents)?;
    Ok(config)
}
```

## Anti-Patterns

### Wrong: Swallowing errors silently

```python
# BAD -- error is caught and discarded; failures become invisible
try:
    result = api.fetch_data()
except Exception:
    pass  # "it's fine"
```

### Correct: Handle or propagate with context

```python
# GOOD -- error is logged with context and re-raised
try:
    result = api.fetch_data()
except ApiError as e:
    logger.error("API fetch failed", exc_info=e)
    raise ServiceUnavailableError("data source offline") from e
```

### Wrong: Using exceptions for flow control

```python
# BAD -- exception used for normal control flow (expensive, confusing)
def find_item(items, target):
    try:
        return items[items.index(target)]
    except ValueError:
        return None
```

### Correct: Use conditional logic for expected cases

```python
# GOOD -- expected case handled with normal control flow
def find_item(items, target):
    return target if target in items else None
```

### Wrong: Stringly-typed errors

```go
// BAD -- comparing error strings is brittle and breaks with any message change
if err.Error() == "record not found" {
    return defaultValue
}
```

### Correct: Use sentinel errors or error types

```go
// GOOD -- type-safe error checking that survives message changes
if errors.Is(err, sql.ErrNoRows) {
    return defaultValue, nil
}
```

### Wrong: Catch-all without re-throw

```typescript
// BAD -- catches everything including programming bugs (TypeError, ReferenceError)
try {
  await processOrder(order);
} catch (e) {
  console.log("something went wrong");
  // swallows the error -- caller never knows it failed
}
```

### Correct: Catch specific errors, re-throw unknown

```typescript
// GOOD -- handles known errors, re-throws unexpected ones
try {
  await processOrder(order);
} catch (e) {
  if (e instanceof ValidationError) {
    return { status: 400, body: e.message };
  }
  if (e instanceof PaymentError) {
    await alertOps(e);
    return { status: 502, body: "Payment processing failed" };
  }
  throw e; // unknown error -- let it propagate
}
```

### Wrong: Losing error context when wrapping

```python
# BAD -- original error is lost; stack trace points here, not the cause
try:
    data = json.loads(raw)
except json.JSONDecodeError:
    raise ValueError("bad input")  # original error is gone
```

### Correct: Chain errors with `from`

```python
# GOOD -- preserves the original error as __cause__
try:
    data = json.loads(raw)
except json.JSONDecodeError as e:
    raise ValueError(f"bad input at position {e.pos}") from e
```

## Common Pitfalls

- **Unhandled Promise rejections in Node.js**: Since Node.js 15, unhandled rejections terminate the process. Fix: Always `await` promises inside `try/catch` or attach `.catch()`. [src7]
- **Go: checking `err == nil` instead of `err != nil`**: Inverted nil check silently proceeds with nil data. Fix: Always verify with `if err != nil { return ..., err }`. [src6]
- **Python: bare `except:` catches `SystemExit` and `KeyboardInterrupt`**: Use `except Exception:` to avoid catching signals and system exits. Fix: Never use bare `except:`, always specify the exception class. [src3]
- **Rust: overusing `.unwrap()`**: Causes panics in production. Fix: Use `?` for propagation, `unwrap_or_default()` for safe fallbacks, reserve `.unwrap()` for tests only. [src1]
- **Java: catching `Exception` in library code**: Catches `RuntimeException` subtypes (NPE, ArrayIndexOutOfBounds) that indicate bugs. Fix: Catch the specific checked exception you expect. [src4]
- **TypeScript: `catch(e)` gives `unknown` type**: Since TypeScript 4.4, catch clause variables are `unknown` by default. Fix: Use `if (e instanceof Error)` type guard before accessing `.message` or `.stack`. [src7]
- **Go: wrapping errors that become part of your API**: Using `%w` makes the wrapped error part of your public API contract. Fix: Use `%v` (not `%w`) for errors you do not want callers to inspect with `errors.Is`. [src2]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building application-layer error handling (HTTP handlers, CLI, workers) | Handling transient network failures with retry logic | Retry with exponential backoff pattern |
| Defining a custom error hierarchy for a library or service | Modeling state transitions with success/failure branches | State machine implementation |
| Choosing between Result types vs exceptions for a new project | Need HTTP error response format (status codes, error bodies) | API error response design pattern |
| Wrapping errors to add context while preserving the original cause | Building circuit breaker or bulkhead resilience | Circuit breaker pattern |
| Setting up global error handlers at application boundaries | Implementing structured logging and alerting | Logging and monitoring system design |

## Important Caveats

- Error handling idioms are language-specific and non-transferable -- what is idiomatic in Rust (Result types) is anti-idiomatic in Python (use exceptions instead)
- The "exceptions vs error values" debate has no universal winner -- each approach has trade-offs in explicitness, performance, and ergonomics that depend on the language ecosystem
- Performance of exceptions varies significantly: Python exceptions are cheap (microseconds), Java exceptions with stack traces are expensive (tens of microseconds) -- in hot paths, this matters
- Error wrapping in Go (`%w`) creates an API contract -- changing the wrapped error in a future version is a breaking change for callers using `errors.Is`

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Circuit Breaker Pattern](/software/patterns/circuit-breaker/2026)
- [Retry with Exponential Backoff](/software/patterns/retry-exponential-backoff/2026)
- [Middleware Pipeline Pattern](/software/patterns/middleware-pipeline/2026)
- [State Machine Implementation](/software/patterns/state-machine-implementation/2026)
- [Node.js Unhandled Promise Rejections](/software/debugging/nodejs-unhandled-promises/2026)
