DEV Community

Cover image for Silent Failures: The Junior Trap You Need to Avoid
Doogal Simpson
Doogal Simpson

Posted on • Originally published at doogal.dev

Silent Failures: The Junior Trap You Need to Avoid

You’ve been there. A user clicks the "Refund" button. The spinner spins. The spinner stops.

Nothing happens.

No error message on the screen. No red text in the console. No alert in your monitoring dashboard. The user clicks it five more times. Still nothing.

You dive into the code, hunting for the bug. You trace the logic through three layers of abstraction until you find it:

if (!success) return false;
Enter fullscreen mode Exit fullscreen mode

This is the Silent Failure. It is the single hardest type of bug to debug because it destroys the evidence at the scene of the crime.

As Juniors, we are often "Optimistic." We write code for the Happy Path—where the database always connects, the order always exists, and the API always responds in 200ms. We return false or null because we are scared of crashing the app.

As Professionals, we must be "Paranoid." We assume the network is congested, the database is exhausted, and the inputs are malicious.

Today, I’m going to teach you The Integrity Check. It’s a pattern to move from fragile, optimistic logic to robust, defensive engineering.

The Junior Trap: The Optimistic Return

Let’s look at a function designed to process a refund.

The "Optimistic Junior" writes code that assumes the order exists and the state is valid. If something is wrong, they return false to keep the application "alive."

// Before: The Optimistic Junior
// The Problem: Silent failures and no validation.

async function processRefund(orderId) {
  const order = await db.getOrder(orderId);

  // If the order doesn't exist... just return false?
  // The caller has no idea WHY it failed. Was it the DB? The ID?
  if (!order) {
    return false; 
  }

  // Business Logic mixed with control flow
  if (order.status === 'completed') {
    await bankApi.refund(order.amount);
    return true;
  }

  // If status is 'pending', we fail silently again.
  return false;
}
Enter fullscreen mode Exit fullscreen mode

Why this fails the "Sleep-Deprived Senior Test"

Imagine it’s 3 AM. The support ticket says "Refunds aren't working." You look at the logs. There are no errors. You look at the code. It just returns false.

Did the refund fail because the ID was wrong? Because the order was already refunded? Because the bank API is down?

You have to guess. That is unacceptable.

The Pro Move: The Integrity Check

To fix this, we need to combine Paranoia with Loudness.

  1. Paranoia: We don't trust the input or the state. We verify it immediately.
  2. Loudness: If the function cannot do what its name says it does, it should scream (throw an error).

We are going to refactor this using Guard Clauses and Explicit Errors.

The Transformation

Here is how a Professional writes the same function.

// After: The Professional Junior
// The Fix: Loud failures, defensive coding, and context.

async function processRefund(orderId) {
  const order = await db.getOrder(orderId);

  // 1. Guard Clauses (Defensive Coding)
  // We stop execution immediately if the data is missing.
  if (!order) {
    throw new Error(`Refund failed: Order ${orderId} not found.`);
  }

  // 2. State Validation
  // We explicitly check for invalid states and throw specific errors.
  if (order.status !== 'completed') {
    throw new Error(
      `Refund failed: Order is ${order.status}, not completed.`
    );
  }

  // 3. Handle External Chaos
  // We wrap third-party calls to add context to generic network errors.
  try {
    await bankApi.refund(order.amount);
  } catch (error) {
    // Wrap the generic error with context so we know WHERE it failed
    throw new Error(`Bank Gateway Error: ${error.message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Why This is Better

1. The Guard Clause Pattern

Notice how we inverted the if statements. Instead of nesting our logic inside if (success) { ... }, we check for failure first and exit immediately.

  • Junior: Checks for the happy path (if (exists)).
  • Pro: Checks for the unhappy path (if (!exists)).

This flattens the code and removes indentation, making it easier to scan (Visual Geography).

2. The Law of Loudness

In the After example, we never return false.

  • If the ID is wrong, we get: Refund failed: Order 123 not found.
  • If the order is pending, we get: Refund failed: Order is pending, not completed.

The error message tells us exactly how to fix the bug. We don't need to debug the code; we just need to read the log.

3. Contextual Wrappers

Third-party APIs usually throw generic errors like 500 Server Error. If we just let that bubble up, we don't know if it was the User Service, the Bank, or the Emailer.

By wrapping the bankApi call in a try/catch, we prepend context: Bank Gateway Error: .... Now we know exactly which integration is acting up.

The Takeaway

The next time you are tempted to type return null or return false when something goes wrong, stop. Ask yourself:

"If this happens at 3 AM, will I know why?"

If the answer is no, throw an error. Code that complains loud and early is code that is easy to maintain. Be paranoid. Be loud. Be professional.


Stop writing code just to please the compiler.

This article was an excerpt from my handbook, "The Professional Junior: Writing Code that Matters."

It’s not a 400-page textbook. It’s a tactical field guide to unwritten engineering rules.

👉 Get the Full Handbook Here

Top comments (0)