Custom Error Classes
Custom Error Classes
Typed Error Hierarchy
class AppError extends Error {
constructor(
public readonly code: string,
message: string,
public readonly statusCode: number = 500,
public readonly isOperational: boolean = true
) {
super(message);
this.name = 'AppError';
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(
message: string,
public readonly fields: Record<string, string[]>
) {
super('VALIDATION_ERROR', message, 400);
this.name = 'ValidationError';
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string | number) {
super('NOT_FOUND', `${resource} with id ${id} not found`, 404);
this.name = 'NotFoundError';
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super('UNAUTHORIZED', message, 401);
this.name = 'UnauthorizedError';
}
}
Result Type Pattern
Result Type Pattern
Explicit Error Handling
type Result<T, E = AppError> =
| { ok: true; value: T }
| { ok: false; error: E };
function Ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function Err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Usage
function findUser(id: number): Result<User, NotFoundError> {
const user = users.find(u => u.id === id);
if (!user) {
return Err(new NotFoundError('User', id));
}
return Ok(user);
}
const result = findUser(1);
if (result.ok) {
console.log(result.value.name); // safe access
} else {
console.error(result.error.message); // safe access
}
Async Result
async function fetchUser(id: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return Err(new AppError('FETCH_ERROR', 'Failed to fetch user', response.status));
}
const user: User = await response.json();
return Ok(user);
} catch (error) {
return Err(new AppError('NETWORK_ERROR', 'Network error', 500));
}
}
Discriminated Error Unions
Discriminated Error Unions
Error State Management
interface LoadingState {
status: 'loading';
}
interface SuccessState<T> {
status: 'success';
data: T;
}
interface ErrorState {
status: 'error';
error: AppError;
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
// Usage in component
function UserComponent({ userId }: { userId: number }) {
const [state, setState] = useState<AsyncState<User>>({ status: 'loading' });
useEffect(() => {
fetchUser(userId).then(result => {
if (result.ok) {
setState({ status: 'success', data: result.value });
} else {
setState({ status: 'error', error: result.error });
}
});
}, [userId]);
switch (state.status) {
case 'loading': return <Spinner />;
case 'error': return <ErrorMessage error={state.error} />;
case 'success': return <UserCard user={state.data} />;
}
}
Exhaustive Error Handling
function handleAsyncState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'loading': return 'Loading...';
case 'success': return `Loaded: ${state.data}`;
case 'error': return `Error: ${state.error.message}`;
default: {
const _exhaustive: never = state;
return _exhaustive;
}
}
}
Error Recovery Patterns
Error Recovery Patterns
Retry Logic
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<Result<T>> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const value = await fn();
return Ok(value);
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
}
}
}
return Err(new AppError(
'RETRY_EXHAUSTED',
`Failed after ${maxRetries} retries: ${lastError!.message}`,
500
));
}
// Usage
const result = await withRetry(() => fetch('https://api.example.com/data'));
Error Boundary Pattern
function catchAsync<T>(
fn: () => Promise<T>,
errorHandler: (error: Error) => T
): Promise<T> {
return fn().catch(error => errorHandler(error));
}
// Usage
const data = await catchAsync(
() => fetchData(),
(error) => {
logger.error('Fetch failed:', error);
return defaultData;
}
);
Best Practices
- Use Result type for operations that can fail predictably
- Create specific error classes for different failure modes
- Never swallow errors silently - log or propagate
- Use discriminated unions for async state management
- Make error handling explicit in function signatures