Luna Interfaces

Blog Details

Why Quick Fixes Are the Most Expensive Code You Will Ever Write

Luna Interfaces
AuthorLuna Interfaces
May 13, 2025
8 min read
Why Quick Fixes Are the Most Expensive Code You Will Ever Write

The app crashes. Users are complaining. Your lead is watching. You look at the stack trace, you find the offending line, and within five minutes you see a way to make the error stop. It's a small change. One condition, maybe two lines. You test it, the crash disappears, you open a pull request. Everyone is relieved. The quick fix is one of the most seductive patterns in software development — it feels like competence under pressure. The problem is that you didn't fix anything. You just moved it somewhere harder to see.

Treating Symptoms Instead of Causes

There's a useful analogy from medicine: if a patient comes in with severe headaches, you can give them painkillers. The pain goes away, they feel better, they go home. But if the headache is caused by a brain tumor, the painkiller didn't just fail to help — it bought the tumor more time. In software, a quick fix is the painkiller. It alleviates the visible symptom — the crash, the error, the failing test — while leaving the underlying cause completely intact and now hidden beneath a layer of compensation code. The next time that cause produces a symptom, it might be somewhere else entirely, dressed up as a completely unrelated problem. And now you have two things to debug instead of one.

The Thinking Behind the Patch

Here's the scenario. Your app processes orders. Somewhere in production, you're getting null reference crashes because getUser() is returning null when a session expires mid-request. A user is halfway through checkout, their session silently times out, and the system tries to place an order for nobody — and blows up. You look at it and think: this is simple. If getUser() returns null, I'll fall back to a guest user. The guest user exists in the system, it has default values for everything, the order flow won't crash. It's defensive programming. You're handling an edge case gracefully. The logic feels sound.

The patch: handle the null, keep the flow going

order.service.tstypescript
1async function processOrder(orderId: string, userId: string) {
2  // 'Fix': if session expired and user is null, fall back to guest
3  const user = await getUser(userId) ?? await getGuestUser();
4
5  const order = await getOrder(orderId);
6
7  await placeOrder(order, user);
8  await sendConfirmationEmail(user.email);         // guest email: ""
9  await chargePaymentMethod(user.paymentMethodId); // guest has no payment method
10  await recordOrderOwner(order.id, user.id);       // order saved under guest id
11}

Why This Is Wrong

The crash is gone. But look at what's actually happening now. When a session expires mid-checkout, the system silently swaps the real user for a guest and continues as if nothing happened. The confirmation email goes to an empty string — it never arrives. The charge attempt hits a guest account with no payment method, which either throws a silent error or fails without any feedback. The order gets recorded under the guest user's ID, permanently disconnected from the actual customer who placed it. The original bug — the session expiring during checkout — is completely untouched. It's still happening. You just made it invisible. Worse, it's now producing financial data corruption: orphaned orders, failed charges that look like successful ones, customers who paid but have no record of their purchase. The quick fix didn't reduce risk. It transformed a loud, obvious crash into a quiet, dangerous data integrity problem.

Stop — Ask Why Before You Ask How

At this point most developers jump to the next patch: catch the null, throw a SessionExpiredError, redirect to login. It's cleaner than the guest fallback, but it's still treating the symptom. Throwing an error doesn't fix anything — it just fails more explicitly. The real question nobody is asking is: why is a session expiring in the middle of an active request at all? A user who just clicked 'Place Order' is clearly active. They didn't go idle. They didn't close the tab. A session system that can expire under those conditions has a design problem, and that's where the actual fix belongs — not in the order flow, not in an error handler, but in the session layer itself.

The Correct Solution: Make the Session Behave Correctly

The root cause is a session TTL that doesn't account for active operations. The fix is a session refresh mechanism: every authenticated request should extend the session TTL, so a user actively moving through a flow never expires mid-operation. For critical flows like checkout, you can extend the TTL even further when the operation begins. This way, getUser() never returns null for an active user — not because you handled the null, but because the null shouldn't be possible in the first place. That's the difference between fixing a problem and compensating for it.

The root fix: sessions that stay alive during active use

session.middleware.tstypescript
1// The fix lives in the session layer — not in the order flow.
2// Refresh the TTL on every authenticated request so an
3// active user can never expire mid-operation.
4
5export async function sessionRefreshMiddleware(
6  req: Request,
7  res: Response,
8  next: NextFunction
9) {
10  const session = await getSession(req);
11
12  if (session?.userId) {
13    await session.refresh(); // resets expiry from now on every request
14  }
15
16  next();
17}
18
19// For long operations like checkout, extend further when they begin
20export async function extendSessionForCheckout(
21  req: Request,
22  res: Response,
23  next: NextFunction
24) {
25  const session = await getSession(req);
26
27  if (session?.userId) {
28    await session.extendTTL(CHECKOUT_SESSION_DURATION);
29  }
30
31  next();
32}
33
34// Now processOrder is clean — no null handling needed,
35// because the session system guarantees an active user stays active
36async function processOrder(orderId: string, userId: string) {
37  const user = await getUser(userId);
38  const order = await getOrder(orderId);
39
40  await placeOrder(order, user);
41  await sendConfirmationEmail(user.email);
42  await chargePaymentMethod(user.paymentMethodId);
43  await recordOrderOwner(order.id, user.id);
44}

The Snowball

Quick fixes don't stay isolated. The first patch teaches the next developer that null users are handled downstream — so they don't check for them either. A month later, someone adds an invoice generation step and assumes the user is always valid. Another month and there's an analytics event that reads user.segment, which doesn't exist on the guest object. Each new layer assumes the previous patches are load-bearing. Nobody fully understands why the guest fallback is there anymore. Removing it to fix the real problem now requires auditing everything that depends on it. The technical debt stopped being about one null check. It became about a shadow assumption baked into your entire order flow. That's the snowball. It doesn't announce itself. It just grows, until someone has to stop everything and excavate.

"Shipping first-time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite. The danger occurs when the debt is not repaid."

W
Ward CunninghamCreator of the Technical Debt metaphor

A Quick Fix Is a Decision — Own It

Sometimes a patch is the right call. A critical bug in production at 2am, with users blocked, is not the moment for a full architectural refactor. The problem isn't the patch itself — it's treating it as the final answer. If you apply a quick fix, log it. Open a ticket. Leave a comment that explains exactly what the patch is compensating for and why the root cause still exists. Make the debt visible so it gets repaid. The most expensive code you'll ever write isn't complex code or over-engineered code. It's the two-line fix that buries a real problem under something that looks like a solution — and gets merged into main on a Friday afternoon.

L
U
N
A

BUILD WHAT MATTERS.