Type-safe error handling with discriminated unions and Result types. Use when designing error handling strategies.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers type-safe error handling patterns for TypeScript.
Use this skill when:
ERRORS ARE VALUES - Treat errors as first-class values that can be typed, returned, and handled explicitly.
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Helper functions
function ok<T>(data: T): Result<T, never> {
return { success: true, data };
}
function err<E>(error: E): Result<never, E> {
return { success: false, error };
}
interface User {
id: string;
name: string;
}
interface UserNotFoundError {
type: 'USER_NOT_FOUND';
userId: string;
}
interface DatabaseError {
type: 'DATABASE_ERROR';
message: string;
}
type GetUserError = UserNotFoundError | DatabaseError;
async function getUser(id: string): Promise<Result<User, GetUserError>> {
try {
const user = await db.users.findUnique({ where: { id } });
if (!user) {
return err({ type: 'USER_NOT_FOUND', userId: id });
}
return ok(user);
} catch (e) {
return err({ type: 'DATABASE_ERROR', message: String(e) });
}
}
// Usage
const result = await getUser('123');
if (result.success) {
console.log(result.data.name); // User
} else {
switch (result.error.type) {
case 'USER_NOT_FOUND':
console.log(`User ${result.error.userId} not found`);
break;
case 'DATABASE_ERROR':
console.log(`Database error: ${result.error.message}`);
break;
}
}
// Base error interface
interface BaseError {
type: string;
message: string;
timestamp: Date;
}
// Specific error types
interface ValidationError extends BaseError {
type: 'VALIDATION_ERROR';
field: string;
value: unknown;
}
interface NotFoundError extends BaseError {
type: 'NOT_FOUND_ERROR';
resource: string;
id: string;
}
interface AuthorizationError extends BaseError {
type: 'AUTHORIZATION_ERROR';
requiredRole: string;
userRole: string;
}
interface NetworkError extends BaseError {
type: 'NETWORK_ERROR';
url: string;
statusCode?: number;
}
// Union of all errors
type AppError =
| ValidationError
| NotFoundError
| AuthorizationError
| NetworkError;
function createValidationError(
field: string,
value: unknown,
message: string,
): ValidationError {
return {
type: 'VALIDATION_ERROR',
field,
value,
message,
timestamp: new Date(),
};
}
function createNotFoundError(resource: string, id: string): NotFoundError {
return {
type: 'NOT_FOUND_ERROR',
resource,
id,
message: `${resource} with id ${id} not found`,
timestamp: new Date(),
};
}
function handleError(error: unknown): AppError {
// Error instance
if (error instanceof Error) {
return {
type: 'NETWORK_ERROR',
url: '',
message: error.message,
timestamp: new Date(),
};
}
// String error
if (typeof error === 'string') {
return {
type: 'NETWORK_ERROR',
url: '',
message: error,
timestamp: new Date(),
};
}
// Unknown error
return {
type: 'NETWORK_ERROR',
url: '',
message: 'An unknown error occurred',
timestamp: new Date(),
};
}
async function fetchData<T>(url: string): Promise<Result<T, NetworkError>> {
try {
const response = await fetch(url);
if (!response.ok) {
return err({
type: 'NETWORK_ERROR',
url,
statusCode: response.status,
message: `HTTP ${response.status}`,
timestamp: new Date(),
});
}
const data = (await response.json()) as T;
return ok(data);
} catch (error) {
return err({
type: 'NETWORK_ERROR',
url,
message: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date(),
});
}
}
abstract class AppErrorBase extends Error {
abstract readonly type: string;
readonly timestamp: Date;
constructor(message: string) {
super(message);
this.name = this.constructor.name;
this.timestamp = new Date();
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationErrorClass extends AppErrorBase {
readonly type = 'VALIDATION_ERROR' as const;
constructor(
message: string,
public readonly field: string,
public readonly value: unknown,
) {
super(message);
}
}
class NotFoundErrorClass extends AppErrorBase {
readonly type = 'NOT_FOUND_ERROR' as const;
constructor(
public readonly resource: string,
public readonly id: string,
) {
super(`${resource} with id ${id} not found`);
}
}
function isValidationError(error: unknown): error is ValidationErrorClass {
return error instanceof ValidationErrorClass;
}
function isNotFoundError(error: unknown): error is NotFoundErrorClass {
return error instanceof NotFoundErrorClass;
}
function isAppError(
error: unknown,
): error is ValidationErrorClass | NotFoundErrorClass {
return isValidationError(error) || isNotFoundError(error);
}
async function safeAsync<T>(
fn: () => Promise<T>,
): Promise<Result<T, Error>> {
try {
const data = await fn();
return ok(data);
} catch (error) {
return err(error instanceof Error ? error : new Error(String(error)));
}
}
// Usage
const result = await safeAsync(() => fetchUser('123'));
if (result.success) {
console.log(result.data);
}
async function getAllResults<T, E>(
operations: Promise<Result<T, E>>[],
): Promise<Result<T[], E>> {
const results = await Promise.all(operations);
const errors = results.filter(
(r): r is { success: false; error: E } => !r.success,
);
if (errors.length > 0) {
return err(errors[0].error);
}
const data = results
.filter((r): r is { success: true; data: T } => r.success)
.map((r) => r.data);
return ok(data);
}
interface ErrorLog {
type: string;
message: string;
timestamp: string;
stack?: string;
context?: Record<string, unknown>;
}
function logError(error: AppError, context?: Record<string, unknown>): void {
const log: ErrorLog = {
type: error.type,
message: error.message,
timestamp: error.timestamp.toISOString(),
context,
};
if (error instanceof Error) {
log.stack = error.stack;
}
console.error(JSON.stringify(log));
}
interface RetryOptions {
maxAttempts: number;
delayMs: number;
backoffMultiplier: number;
}
async function withRetry<T, E>(
fn: () => Promise<Result<T, E>>,
options: RetryOptions,
): Promise<Result<T, E>> {
let lastError: E | undefined;
let delay = options.delayMs;
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
const result = await fn();
if (result.success) {
return result;
}
lastError = result.error;
if (attempt < options.maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= options.backoffMultiplier;
}
}
return err(lastError!);
}
function withFallback<T, E>(
result: Result<T, E>,
fallback: T,
): T {
return result.success ? result.data : fallback;
}
function withFallbackFn<T, E>(
result: Result<T, E>,
fallbackFn: (error: E) => T,
): T {
return result.success ? result.data : fallbackFn(result.error);
}
unknown type