Error Handling Patterns
Learn common error handling patterns and best practices
Error Handling Patterns
This guide covers common error handling patterns and best practices when using the go-errors library.
Core Principles
- Early Returns: Check for errors immediately and return early
- Error Propagation: Pass errors up the call stack with context
- Type Safety: Use custom error types with meaningful context
- Immutability: Results are readonly tuples
- Consistency: Use
let
for declarations and reuse error variables - Error Context: Preserve error context when propagating
Basic Patterns
Synchronous Error Handling
import { goSync } from 'go-errors';
function divide(a: number, b: number) {
let [result, err] = goSync(() => {
if (b === 0) throw new Error('Division by zero');
return a / b;
});
if (err) return [null, err] as const;
return [result, null] as const;
}
// Usage
let [value, err] = divide(10, 2);
if (err) {
console.error('Division failed:', err.message);
return;
}
console.log('Result:', value);
HTTP Error Handling
import { goFetch } from 'go-errors';
interface User {
id: string;
firstName: string;
lastName: string;
fullName: string;
}
interface ApiError {
code: string;
message: string;
details?: unknown;
}
async function fetchUserData(userId: string) {
let [user, err] = await goFetch<User, ApiError>(`/api/users/${userId}`, {
responseTransformer: (data) => ({
...data,
fullName: `${data.firstName} ${data.lastName}`
}),
errorTransformer: (error) => {
if (error instanceof Response) {
return {
code: `HTTP_${error.status}`,
message: error.statusText
};
}
return {
code: 'UNKNOWN_ERROR',
message: error instanceof Error ? error.message : String(error)
};
}
});
if (err) {
console.error(`Failed to fetch user: ${err.code}`, err.message);
return [null, err] as const;
}
return [user, null] as const;
}
Error Types
Custom Error Types
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: unknown
) {
super(message);
this.name = 'ValidationError';
}
}
function validateUser(user: unknown) {
let [result, err] = goSync<unknown, ValidationError>(() => {
if (!user || typeof user !== 'object') {
throw new ValidationError(
'Invalid user object',
'user',
user
);
}
return user;
});
if (err) {
console.error(
`Validation failed: ${err.message}`,
`Field: ${err.field}`,
`Value: ${err.value}`
);
return [null, err] as const;
}
return [result, null] as const;
}
Error Hierarchies
class AppError extends Error {
constructor(
message: string,
public code: string,
public details?: unknown
) {
super(message);
this.name = 'AppError';
}
}
class DatabaseError extends AppError {
constructor(message: string, details?: unknown) {
super(message, 'DATABASE_ERROR', details);
this.name = 'DatabaseError';
}
}
class AuthError extends AppError {
constructor(message: string) {
super(message, 'AUTH_ERROR');
this.name = 'AuthError';
}
}
Error Propagation
With Context
async function getUserData(userId: string) {
let [user, err] = await goFetch<User>(`/api/users/${userId}`);
if (err) {
return [null, new AppError(
'Failed to fetch user data',
'USER_FETCH_ERROR',
{ userId, cause: err }
)] as const;
}
let [validated, err] = goSync(() => validateUser(user));
if (err) {
return [null, new AppError(
'User validation failed',
'USER_VALIDATION_ERROR',
{ userId, cause: err }
)] as const;
}
return [validated, null] as const;
}
Error Transformation
let [data, err] = await goFetch<User, ApiError>('/api/user', {
errorTransformer: (error) => {
if (error instanceof Response) {
return {
code: `HTTP_${error.status}`,
message: error.statusText,
details: { status: error.status }
};
}
if (error instanceof ValidationError) {
return {
code: 'VALIDATION_ERROR',
message: error.message,
details: { field: error.field, value: error.value }
};
}
return {
code: 'UNKNOWN_ERROR',
message: String(error)
};
}
});
Best Practices
- Always use
let
for result declarations:
// ✅ Good: Using let
let [value, err] = goSync(() => validate(input));
if (err) return [null, err] as const;
// ❌ Bad: Using const
const [data, error] = goSync(() => validate(input));
- Add meaningful context to errors:
// ✅ Good: Rich error context
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: unknown
) {
super(message);
}
}
// ❌ Bad: Just message
throw new Error('Validation failed');
- Use error hierarchies for better error handling:
// ✅ Good: Error hierarchy
class AppError extends Error {
constructor(message: string, public code: string) {
super(message);
}
}
class ValidationError extends AppError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR');
}
}
// ❌ Bad: Flat error types
class ValidationError extends Error {}
class NetworkError extends Error {}
- Transform errors consistently:
// ✅ Good: Consistent error transformation
let [data, err] = await goFetch<User, ApiError>('/api/user', {
errorTransformer: (error) => ({
code: error instanceof Response ? `HTTP_${error.status}` : 'UNKNOWN',
message: error instanceof Error ? error.message : String(error)
})
});
// ❌ Bad: Inconsistent error handling
let [data, err] = await goFetch('/api/user');
if (err instanceof Response) {
// Handle response error
} else if (err instanceof Error) {
// Handle error
}