Stop Writing Defensive Null Checks: Use the Rust-Inspired Result Pattern in TypeScript


In standard JavaScript and TypeScript, functions throw exceptions implicitly. When a function signature looks like:


function parseConfig(raw: string): AppConfig { ... }


The type system tells you nothing about failure modes. If parsing fails, it throws at runtime. The caller has no idea it needs a try/catch until production logs blow up with uncaught exceptions.


The Problem with Exceptions for Expected Errors
Exceptions should be reserved for exceptional, unrecoverable system failures (e.g., out-of-memory, network hardware drop). Domain failures—like validation errors, failed lookups, or bad payloads—are expected states. Treating them as thrown exceptions destroys type safety and complicates control flow.


The Fix: Explicit Result<T, E>
Borrowing from Rust’s Result<T, E>, we model success and failure as explicit return values instead of hidden throws.


// 1. Define the algebraic Result type
export type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };


export const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const Err = <E>(error: E): Result<never, E> => ({ ok: false, error });


How to Implement It in Real Code:
Instead of throwing inside your business logic, wrap your outcomes:


interface ValidationError {
field: string;
reason: string;
}


function parsePort(input: string): Result<number, ValidationError> {
const port = Number(input);

if (isNaN(port) || port <= 0 || port > 65535) {
return Err({ field: "PORT", reason: "Must be a valid integer between 1 and 65535" });
}


return Ok(port);
}


How It Enforces Safe Consuming:
TypeScript’s discriminated union forces the developer to check result.ok before accessing result.value. Attempting to read result.value when ok: false is rejected at compile time:


TypeScript
const result = parsePort(process.env.APP_PORT ?? "");


if (!result.ok) {
// TypeScript knows 'result.error' is safely typed here
console.error(`Config failure on ${result.error.field}: ${result.error.reason}`);
process.exit(1);
}


// Compiler guarantees 'result.value' exists and is a number
const port = result.value;
server.listen(port);


Why This Upgrades Your Architecture:
Self-Documenting Signatures: Callers instantly see every failure mode in their IDE without reading source code.


Zero Uncaught Explosions: Errors are treated as normal control flow, making multi-step pipelines easily composable.


Deterministic Testing: You test predictable data structures rather than asserting whether a method threw an exception.


Discussion Question
Do you rely on explicit union types/monadic patterns like Result in your TypeScript backend, or do you still prefer native try/catch and custom exception classes? What tradeoffs have you seen in large codebases?


CTA (Join Developers & Coding)
Ready to sharpen your software craft with modern architecture patterns, typed systems, and clean code principles? Join the Developers & Coding community to collaborate on production patterns with peers worldwide.
Stop Writing Defensive Null Checks: Use the Rust-Inspired Result Pattern in TypeScript In standard JavaScript and TypeScript, functions throw exceptions implicitly. When a function signature looks like: function parseConfig(raw: string): AppConfig { ... } The type system tells you nothing about failure modes. If parsing fails, it throws at runtime. The caller has no idea it needs a try/catch until production logs blow up with uncaught exceptions. The Problem with Exceptions for Expected Errors Exceptions should be reserved for exceptional, unrecoverable system failures (e.g., out-of-memory, network hardware drop). Domain failures—like validation errors, failed lookups, or bad payloads—are expected states. Treating them as thrown exceptions destroys type safety and complicates control flow. The Fix: Explicit Result<T, E> Borrowing from Rust’s Result<T, E>, we model success and failure as explicit return values instead of hidden throws. // 1. Define the algebraic Result type export type Result<T, E = Error> = | { ok: true; value: T } | { ok: false; error: E }; export const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value }); export const Err = <E>(error: E): Result<never, E> => ({ ok: false, error }); How to Implement It in Real Code: Instead of throwing inside your business logic, wrap your outcomes: interface ValidationError { field: string; reason: string; } function parsePort(input: string): Result<number, ValidationError> { const port = Number(input); if (isNaN(port) || port <= 0 || port > 65535) { return Err({ field: "PORT", reason: "Must be a valid integer between 1 and 65535" }); } return Ok(port); } How It Enforces Safe Consuming: TypeScript’s discriminated union forces the developer to check result.ok before accessing result.value. Attempting to read result.value when ok: false is rejected at compile time: TypeScript const result = parsePort(process.env.APP_PORT ?? ""); if (!result.ok) { // TypeScript knows 'result.error' is safely typed here console.error(`Config failure on ${result.error.field}: ${result.error.reason}`); process.exit(1); } // Compiler guarantees 'result.value' exists and is a number const port = result.value; server.listen(port); Why This Upgrades Your Architecture: Self-Documenting Signatures: Callers instantly see every failure mode in their IDE without reading source code. Zero Uncaught Explosions: Errors are treated as normal control flow, making multi-step pipelines easily composable. Deterministic Testing: You test predictable data structures rather than asserting whether a method threw an exception. Discussion Question Do you rely on explicit union types/monadic patterns like Result in your TypeScript backend, or do you still prefer native try/catch and custom exception classes? What tradeoffs have you seen in large codebases? CTA (Join Developers & Coding) Ready to sharpen your software craft with modern architecture patterns, typed systems, and clean code principles? Join the Developers & Coding community to collaborate on production patterns with peers worldwide.
0 Comments 0 Shares 40 Views 0 Reviews