惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

M
MIT News - Artificial intelligence
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
J
Java Code Geeks
G
Google Developers Blog
美团技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
The Blog of Author Tim Ferriss
月光博客
月光博客
B
Blog
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
博客园_首页
人人都是产品经理
人人都是产品经理
aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
Jina AI
Jina AI
S
SegmentFault 最新的问题
H
Help Net Security
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
Google DeepMind News
Google DeepMind News

jola.dev

Migrating your Bluesky account the hard way | jola.dev Cluster singleton pattern | jola.dev cove.town, atproto self-hosted self-hosting | jola.dev Speeding up a Phoenix LiveView web app with a CDN | jola.dev Self-hosting an atproto container registry | jola.dev Migrating to the new Tangled knot2 | jola.dev Self-hosting and Tangled | jola.dev Self-hosting your PDS | jola.dev Taking control of your atproto account | jola.dev No cost, no value | jola.dev Latch - an Elixir atproto OAuth library | jola.dev Limited output is a feature | jola.dev Distributed rate limiter with HRW in Elixir | jola.dev A computer can never be held accountable | jola.dev Elixir Cluster 101 | jola.dev How to stop Claude from saying load-bearing | jola.dev Let libraries be libraries | jola.dev CI workflows on Tangled for Elixir | jola.dev Automatically syncing your blog to atproto and standard.site | jola.dev Appreciation for the small web | jola.dev Treating LLMs as programming books Publishing your blog to standard.site in Elixir Generating OG images in Elixir The social contract of writing Highest Random Weight in Elixir bunnyx: a bunny.net Elixir client library Building for the joy of building Running local models on an M4 with 24GB memory How to hit your Claude weekly limit so you can go outside and touch grass Dropping Cloudflare for bunny.net
If the Goal is Resiliency, Defensive Programming is Your ...
Johanna Larsson · 2025-07-21 · via jola.dev

Disclaimer: This blog post was written in 2025 during a time when I experimented with LLM-assisted writing.

This may sound incredibly unintuitive, but if you want to build resilient software, conditionals are your worst enemy.

I've worked on enough projects to see this pattern play out over and over. Teams start with the best intentions, trying to handle every edge case, wrap every operation in error handling, and provide fallbacks for every possible failure. But what they end up with is something far worse than the occasional crash: a system that lies to you.

Two Philosophies of Software Design

Let me paint you a picture of two different projects I've encountered, each following a completely different philosophy.

Project 1: The Defensive Approach

This team believed in defensive programming with religious fervor. Every line of code was wrapped in error handling. Every exception caught and swallowed. Every piece of missing data had a default behavior.

When they looked up a session in the browser and it didn't exist, they'd return an empty object. What if the user object in the session was nil? No problem, just use a default value! And just to be absolutely sure nothing could go wrong, they'd wrap the whole thing in a try-catch block.

The codebase became a maze of conditionals. Nobody knew which fields were supposed to exist and which ones were optional. The team's focus was entirely on preventing incidents from happening at all. But when things inevitably went wrong, everyone was completely lost. The errors had been caught and handled so many times that the original problem was buried under layers of "defensive" code.

Project 2: The Explicit Approach

This team took a radically different approach. They encoded every variant directly into their codebase. Every line of code expressed clearly whether a field was guaranteed to exist or not. No hidden assumptions, no silent defaults.

Sure, they had bugs. Sometimes data wasn't where it was supposed to be. But when that happened, the system failed immediately at the point of the problem, not three layers deep in some unrelated module. Every feature was built with the explicit acknowledgment that things can and will crash. The focus wasn't on preventing incidents but on recovering from them quickly.

What Actually Happens in Practice

Here's what I've learned after dealing with both approaches in production systems.

Project 1 creates systems that silently degrade. Users get blank screens, incomplete data, or weird behavior that's almost impossible to reproduce. Your monitoring shows everything is "fine" because no exceptions were thrown. You're not debugging real problems anymore; you're debugging ghosts. Issues that happened three layers deep in your abstraction, masked by well-meaning error handlers that turned a clear failure into a subtle corruption of state.

Project 2 creates systems that fail fast and loud. When something breaks, you know exactly where and why. Your error monitoring lights up like a Christmas tree, but here's the thing: you can trace the issue back to its source in minutes instead of hours. The stack trace tells you the real story, not some sanitized version of it.

The Counterintuitive Truth

The real kicker? Project 2 systems actually have fewer outages. Not because they're more defensive, but because they're more honest about what can go wrong.

When your system fails fast, you find and fix problems quickly. When your system silently handles errors, those problems compound. That defensive nil check you added? It's now hiding the fact that your authentication system is returning malformed data. That try-catch block around your database call? It's masking connection pool exhaustion until your entire system grinds to a halt.

A Better Way Forward

Stop trying to catch every possible error. Start being explicit about what your code expects, and let it crash when those expectations aren't met. This doesn't mean being reckless. It means being honest.

If a function expects a user ID, don't provide a default value when it's missing. If a service call can fail, make that failure explicit in your type system or return values. When something goes wrong, let it bubble up to a level where it can be meaningfully handled, with all the context intact.

Your monitoring will be more accurate. Your debugging will be faster. And counterintuitively, your system will be more reliable because problems get fixed instead of hidden.

Your future debugging self will thank you when you're looking at a clear error message with a full stack trace at 3 AM, instead of trying to figure out why users are seeing empty screens with no errors in the logs.

Have you experienced this in your own projects? I'd love to hear about your experiences with fail-fast versus defensive programming approaches. The tension between preventing errors and handling them properly is one of the fundamental challenges in building reliable systems, and I'm always curious to hear how other teams navigate it.

Written by Johanna Larsson. Thoughts on this post? Find me on Bluesky at @jola.dev or why not give it a vote on Bubbles.