Result<T, E> (Rust), if err != nil (Go), try/except (Python), try/catch with custom Error classes (TypeScript)| 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) |
START
+-- Is the error recoverable?
| +-- YES
| | +-- Language uses Result types (Rust, Go)?
| | | +-- Rust --> Return Result<T, E>, caller uses ? or match
| | | +-- Go --> Return (value, error), caller checks if err != nil
| | +-- Language uses exceptions (Python, JS, Java, C#)?
| | +-- Can the caller reasonably handle it?
| | | +-- YES --> Throw specific exception, document in signature
| | | +-- NO --> Let it propagate to a higher-level handler
| | +-- Is it a validation/user-input error?
| | +-- YES --> Return error object/Result, don't throw
| | +-- NO --> Throw typed exception
| +-- NO (fatal/unrecoverable)
| +-- Rust --> panic!() -- process should crash
| +-- Go --> log.Fatal() or panic() with deferred cleanup
| +-- Python/JS/Java/C# --> Let exception propagate to top-level handler
+-- Should you log or propagate?
| +-- Low-level library/module --> Propagate (wrap with context), do NOT log
| +-- Application boundary (HTTP handler, CLI entrypoint) --> Log AND return error response
| +-- Background worker --> Log, report to error tracker, optionally retry
+-- Should you retry?
+-- Transient error (network timeout, rate limit, lock contention)?
| +-- YES --> Retry with exponential backoff (see retry-exponential-backoff unit)
| +-- NO (validation, auth, logic error) --> Do NOT retry, fail fast
+-- END
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: error hierarchy
class AppError(Exception):
def __init__(self, message: str, code: str, cause: Exception | None = None):
super().__init__(message)
self.code = code
self.__cause__ = cause
class ValidationError(AppError):
def __init__(self, field: str, message: str):
super().__init__(message, code="VALIDATION_ERROR")
self.field = field
class NotFoundError(AppError):
def __init__(self, resource: str, identifier: str):
super().__init__(f"{resource} '{identifier}' not found", code="NOT_FOUND")
Verify: raise ValidationError("email", "invalid format") → expected: ValidationError: invalid format
When catching and re-raising errors, wrap the original to maintain the full chain. [src2] [src6]
// 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)
}
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
Catch unhandled errors at the application boundary to prevent crashes and ensure logging. [src7]
// TypeScript/Node.js: global handlers
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
process.exit(1);
});
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
process.exit(1);
});
Verify: Trigger unhandled rejection with Promise.reject(new Error('test')) → should log and exit
Instead of catching all errors and guessing, use type guards or pattern matching to handle specific error types. [src1]
// Rust: pattern matching on Result
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(...))
# 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
// 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 });
}
// Input: File path to read
// Output: File contents or wrapped error
var ErrConfigNotFound = errors.New("config file not found")
type ConfigError struct {
Path string
Err error
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("config error for %s: %v", e.Path, e.Err)
}
func (e *ConfigError) Unwrap() error { return e.Err }
func ReadConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %s", ErrConfigNotFound, path)
}
return nil, &ConfigError{Path: path, Err: err}
}
return data, nil
}
// 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)
}
# BAD -- error is caught and discarded; failures become invisible
try:
result = api.fetch_data()
except Exception:
pass # "it's fine"
# 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
# BAD -- exception used for normal control flow (expensive, confusing)
def find_item(items, target):
try:
return items[items.index(target)]
except ValueError:
return None
# GOOD -- expected case handled with normal control flow
def find_item(items, target):
return target if target in items else None
// BAD -- comparing error strings is brittle and breaks with any message change
if err.Error() == "record not found" {
return defaultValue
}
// GOOD -- type-safe error checking that survives message changes
if errors.Is(err, sql.ErrNoRows) {
return defaultValue, nil
}
// BAD -- catches everything including programming bugs
try {
await processOrder(order);
} catch (e) {
console.log("something went wrong");
}
// 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
}
# BAD -- original error is lost
try:
data = json.loads(raw)
except json.JSONDecodeError:
raise ValueError("bad input") # original error is gone
# 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
await promises inside try/catch or attach .catch(). [src7]if err != nil { return ..., err }. [src6]except Exception: to avoid catching signals and system exits. Fix: Never use bare except:, always specify the exception class. [src3]? for propagation, unwrap_or_default() for safe fallbacks, reserve .unwrap() for tests only. [src1]unknown by default. Fix: Use if (e instanceof Error) type guard before accessing .message or .stack. [src7]%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]| 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 |