Stop Writing Brittle Code: Why Defensive Design Beats Endless Try-Catch Blocks


Most developers are taught error handling as an afterthought: wrap the risky operation, log an error string, and return null or an empty object.


In production systems, this creates Silent State Corruption. When a function returns null, every caller up the stack must remember to check for it. Forget one check, and you trigger an unhandled runtime failure three services away.


The industry standard for resilient systems is moving away from exception-driven flow control toward Result Type Modeling and Parse, Don't Validate.


TypeScript
// ❌ ANTI-PATTERN: Exception-driven flow control
async function getUser(id: string): Promise<User | null> {
try {
const raw = await db.query(id);
return raw as User; // Blind type assertion
} catch (err) {
console.error(err);
return null; // Passes undefined failure state upstream
}
}


// ✅ PRODUCTION READY: Typed Result Pattern + Runtime Parsing
type Result<T, E> = { ok: true; data: T } | { ok: false; error: E };


async function getUserSafe(id: string): Promise<Result<User, DatabaseError | ValidationError>> {
const queryResult = await db.safeQuery(id);
if (!queryResult.ok) {
return { ok: false, error: new DatabaseError(queryResult.msg) };
}


// Parse schema at the boundary; never trust raw inputs
const parsed = UserSchema.safeParse(queryResult.raw);
if (!parsed.success) {
return { ok: false, error: new ValidationError(parsed.error) };
}


return { ok: true, data: parsed.data };
}


Why This Changes Your Codebase:
Compile-Time Enforcement: Callers cannot access result.data without narrowing result.ok === true. The compiler forces you to handle failure explicitly before runtime.


Boundary Validation: By validating data shape immediately at the boundary (API responses, DB queries, message queues) using schemas like Zod or TypeBox, inner domain logic never has to check if properties exist.


Traceable Domain Errors: Instead of generic Error instances, return tagged union error types that document exactly what failure modes an operation can trigger.


Exceptions should be reserved for truly exceptional conditions (e.g., out-of-memory or dropped socket connections), not expected domain states like missing records or invalid inputs.


Discussion Question
How does your team handle domain error propagation in production—Result/Either types, explicit custom exception hierarchies, or middleware-level error boundaries? What trade-offs have you seen in developer velocity?


CTA
Ready to write cleaner, production-grade code that doesn't break at 3 AM?


👉 Join the Techawks Developers & Coding Community to exchange code reviews, architectural design patterns, and engineering practices with active builders.
Stop Writing Brittle Code: Why Defensive Design Beats Endless Try-Catch Blocks Most developers are taught error handling as an afterthought: wrap the risky operation, log an error string, and return null or an empty object. In production systems, this creates Silent State Corruption. When a function returns null, every caller up the stack must remember to check for it. Forget one check, and you trigger an unhandled runtime failure three services away. The industry standard for resilient systems is moving away from exception-driven flow control toward Result Type Modeling and Parse, Don't Validate. TypeScript // ❌ ANTI-PATTERN: Exception-driven flow control async function getUser(id: string): Promise<User | null> { try { const raw = await db.query(id); return raw as User; // Blind type assertion } catch (err) { console.error(err); return null; // Passes undefined failure state upstream } } // ✅ PRODUCTION READY: Typed Result Pattern + Runtime Parsing type Result<T, E> = { ok: true; data: T } | { ok: false; error: E }; async function getUserSafe(id: string): Promise<Result<User, DatabaseError | ValidationError>> { const queryResult = await db.safeQuery(id); if (!queryResult.ok) { return { ok: false, error: new DatabaseError(queryResult.msg) }; } // Parse schema at the boundary; never trust raw inputs const parsed = UserSchema.safeParse(queryResult.raw); if (!parsed.success) { return { ok: false, error: new ValidationError(parsed.error) }; } return { ok: true, data: parsed.data }; } Why This Changes Your Codebase: Compile-Time Enforcement: Callers cannot access result.data without narrowing result.ok === true. The compiler forces you to handle failure explicitly before runtime. Boundary Validation: By validating data shape immediately at the boundary (API responses, DB queries, message queues) using schemas like Zod or TypeBox, inner domain logic never has to check if properties exist. Traceable Domain Errors: Instead of generic Error instances, return tagged union error types that document exactly what failure modes an operation can trigger. Exceptions should be reserved for truly exceptional conditions (e.g., out-of-memory or dropped socket connections), not expected domain states like missing records or invalid inputs. Discussion Question How does your team handle domain error propagation in production—Result/Either types, explicit custom exception hierarchies, or middleware-level error boundaries? What trade-offs have you seen in developer velocity? CTA Ready to write cleaner, production-grade code that doesn't break at 3 AM? 👉 Join the Techawks Developers & Coding Community to exchange code reviews, architectural design patterns, and engineering practices with active builders.
0 Comentários 0 Compartilhamentos 18 Visualizações 0 Anterior