Stop Leaking Unchecked Exceptions: How to Build Bulletproof APIs with the Result Pattern in TypeScript
Throwing exceptions for expected domain failures (e.g., validation errors, expired sessions, or resource conflicts) violates one of software engineering's most fundamental tenets: type safety across execution boundaries.


When a function signatures looks like this:


TypeScript
async function processOrder(orderId: string): Promise<Order>
The TypeScript compiler has no idea what could go wrong. It forces consumers to guess what exceptions need catching, resulting in swallowed errors, missing fallback states, and unexpected 500 HTTP crashes.


The Architectural Shift: Explicit Error Modeling
Languages like Rust make failure paths explicit at the type level using the Result<T, E> algebraic data type. You can implement this cleanly in modern TypeScript without external dependencies using discriminated unions:


TypeScript
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };


// Define discrete, exhaustive error states
type PaymentError =
| { code: 'INSUFFICIENT_FUNDS'; balance: number }
| { code: 'CARD_EXPIRED' }
| { code: 'GATEWAY_TIMEOUT' };


async function chargeCard(amount: number): Promise<Result<Transaction, PaymentError>> {
if (amount > availableFunds) {
return {
success: false,
error: { code: 'INSUFFICIENT_FUNDS', balance: availableFunds }
};
}
// ... execute charge
return { success: true, data: transaction };
}
Why This Changes Your Code Quality:
Exhaustive Type Narrowing: Consumers cannot access result.data without explicitly handling if (!result.success). TypeScript will flag unhandled error conditions at build time.


Deterministic Control Flow: Exceptions are reserved exclusively for truly unrecoverable anomalies (out-of-memory errors, syntax bugs, database connection drops), while expected business failures remain predictable, first-class values.


Decoupled API Contracts: Your HTTP controller can deterministically map every domain error code directly to its respective HTTP status code (e.g., 402 Payment Required, 408 Timeout) via a simple, exhaustive switch block.


Stop treating your failure paths as second-class citizens. If a failure can be predicted, it belongs in the return type.


Discussion Question
In your current backend codebase, do you handle domain-level failures via custom thrown exception hierarchies or explicit Result / Either types—and what was the tipping point for your team?


CTA
Join Developers & Coding: Connect with senior software engineers, backend developers, and systems architects refining clean code patterns, type safety, and scalable architectures inside the Techawks Developers community. Link in bio/comments.
Stop Leaking Unchecked Exceptions: How to Build Bulletproof APIs with the Result Pattern in TypeScript Throwing exceptions for expected domain failures (e.g., validation errors, expired sessions, or resource conflicts) violates one of software engineering's most fundamental tenets: type safety across execution boundaries. When a function signatures looks like this: TypeScript async function processOrder(orderId: string): Promise<Order> The TypeScript compiler has no idea what could go wrong. It forces consumers to guess what exceptions need catching, resulting in swallowed errors, missing fallback states, and unexpected 500 HTTP crashes. The Architectural Shift: Explicit Error Modeling Languages like Rust make failure paths explicit at the type level using the Result<T, E> algebraic data type. You can implement this cleanly in modern TypeScript without external dependencies using discriminated unions: TypeScript type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; // Define discrete, exhaustive error states type PaymentError = | { code: 'INSUFFICIENT_FUNDS'; balance: number } | { code: 'CARD_EXPIRED' } | { code: 'GATEWAY_TIMEOUT' }; async function chargeCard(amount: number): Promise<Result<Transaction, PaymentError>> { if (amount > availableFunds) { return { success: false, error: { code: 'INSUFFICIENT_FUNDS', balance: availableFunds } }; } // ... execute charge return { success: true, data: transaction }; } Why This Changes Your Code Quality: Exhaustive Type Narrowing: Consumers cannot access result.data without explicitly handling if (!result.success). TypeScript will flag unhandled error conditions at build time. Deterministic Control Flow: Exceptions are reserved exclusively for truly unrecoverable anomalies (out-of-memory errors, syntax bugs, database connection drops), while expected business failures remain predictable, first-class values. Decoupled API Contracts: Your HTTP controller can deterministically map every domain error code directly to its respective HTTP status code (e.g., 402 Payment Required, 408 Timeout) via a simple, exhaustive switch block. Stop treating your failure paths as second-class citizens. If a failure can be predicted, it belongs in the return type. Discussion Question In your current backend codebase, do you handle domain-level failures via custom thrown exception hierarchies or explicit Result / Either types—and what was the tipping point for your team? CTA Join Developers & Coding: Connect with senior software engineers, backend developers, and systems architects refining clean code patterns, type safety, and scalable architectures inside the Techawks Developers community. Link in bio/comments.
0 التعليقات 0 المشاركات 19 مشاهدة 0 معاينة