From harness-claude
Models and types errors explicitly using Result types, discriminated unions, and typed throws to replace untyped try/catch with type-safe error handling patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:ts-error-handling-typesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Model and type errors explicitly using Result types, discriminated unions, and typed throws
Model and type errors explicitly using Result types, discriminated unions, and typed throws
type Result<T, E = Error> = { 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 };
}
type ValidationError = { field: string; message: string };
function validateEmail(input: string): Result<string, ValidationError> {
if (!input.includes('@')) {
return err({ field: 'email', message: 'Must contain @' });
}
return ok(input.toLowerCase().trim());
}
const result = validateEmail(input);
if (result.ok) {
sendEmail(result.value); // Type: string
} else {
showError(result.error); // Type: ValidationError
}
type AppError =
| { type: 'NOT_FOUND'; resource: string; id: string }
| { type: 'VALIDATION'; errors: ValidationError[] }
| { type: 'UNAUTHORIZED'; reason: string }
| { type: 'RATE_LIMITED'; retryAfter: number };
function handleError(error: AppError): Response {
switch (error.type) {
case 'NOT_FOUND':
return new Response(`${error.resource} ${error.id} not found`, { status: 404 });
case 'VALIDATION':
return Response.json({ errors: error.errors }, { status: 400 });
case 'UNAUTHORIZED':
return new Response(error.reason, { status: 401 });
case 'RATE_LIMITED':
return new Response('Too many requests', {
status: 429,
headers: { 'Retry-After': String(error.retryAfter) },
});
}
}
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number,
public readonly details?: Record<string, unknown>
) {
super(message);
this.name = 'AppError';
}
static notFound(resource: string, id: string): AppError {
return new AppError(`${resource} ${id} not found`, 'NOT_FOUND', 404);
}
static badRequest(message: string, details?: Record<string, unknown>): AppError {
return new AppError(message, 'BAD_REQUEST', 400, details);
}
}
function isAppError(error: unknown): error is AppError {
return error instanceof AppError;
}
try {
await processRequest();
} catch (error: unknown) {
if (isAppError(error)) {
return handleAppError(error); // Typed as AppError
}
if (error instanceof Error) {
return handleUnexpectedError(error);
}
throw error; // Re-throw unknown errors
}
function andThen<T, U, E>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E> {
return result.ok ? fn(result.value) : result;
}
const result = andThen(validateEmail(input), (email) => checkEmailAvailable(email));
type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
async function fetchUser(id: string): AsyncResult<User, AppError> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return err(AppError.notFound('User', id));
return ok(await res.json());
} catch {
return err(new AppError('Network error', 'NETWORK', 503));
}
}
never for functions that always throw:function fail(message: string): never {
throw new Error(message);
}
function assertDefined<T>(value: T | undefined, name: string): T {
if (value === undefined) fail(`${name} is required`);
return value; // TypeScript knows this is T because fail returns never
}
TypeScript does not have a built-in mechanism for typed exceptions. The catch block always receives unknown (with useUnknownInCatchVariables). The Result pattern brings error typing to function signatures, making error handling explicit and exhaustive.
Result vs try/catch:
andThenError class inheritance: JavaScript's instanceof check works with Error subclasses, but Error.captureStackTrace (V8) must be called in the constructor for proper stack traces:
class CustomError extends Error {
constructor(message: string) {
super(message);
this.name = 'CustomError';
// Fix prototype chain for instanceof checks
Object.setPrototypeOf(this, CustomError.prototype);
}
}
Trade-offs:
never return type enables dead code elimination — but is easy to misusehttps://typescriptlang.org/docs/handbook/2/narrowing.html
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeModels state machines, discriminated unions, Result/Option types, and branded types in TypeScript for type-safe domain modeling.
Master error handling patterns including exceptions, Result types, error propagation, and graceful degradation to build resilient applications. Use when implementing error handling, designing APIs, or improving reliability.
Teaches fp-ts Either and TaskEither to handle errors as values in TypeScript, replacing try/catch with typed propagation. Use for validation, domain errors, and clearer contracts.