Silent Failures: The Junior Trap You Need to Avoid
Source: Dev.to

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;
This is the Silent Failure – the single hardest type of bug to debug because it destroys the evidence at the scene of the crime.
Optimistic vs. Paranoid
- Juniors are often optimistic. They write code for the happy path—where the database always connects, the order always exists, and the API always responds in 200 ms. They return
falseornullbecause they’re scared of crashing the app. - Professionals must be paranoid. They assume the network is congested, the database is exhausted, and the inputs are malicious.
Today, I’m going to teach you The Integrity Check – 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;
}
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.
- Paranoia: We don’t trust the input or the state. We verify it immediately.
- Loudness: If the function cannot do what its name says it does, it should scream (throw an error).
We’ll refactor using Guard Clauses and Explicit Errors.
The Transformation
// After: The Professional Junior
// The Fix: Loud failures, defensive coding, and context.
async function processRefund(orderId) {
const order = await db.getOrder(orderId);
// 1. Guard Clause – stop execution if the data is missing.
if (!order) {
throw new Error(`Refund failed: Order ${orderId} not found.`);
}
// 2. State Validation – ensure the order is in a refundable state.
if (order.status !== 'completed') {
throw new Error(
`Refund failed: Order is ${order.status}, not completed.`
);
}
// 3. Handle External Chaos – wrap third‑party calls to add context.
try {
await bankApi.refund(order.amount);
} catch (error) {
// Add context so we know *where* it failed.
throw new Error(`Bank Gateway Error: ${error.message}`);
}
}
Why This Is Better
1. Guard Clause Pattern
We invert the if statements: instead of nesting logic inside if (success) { … }, we check for failure first and exit immediately. This flattens the code, removes indentation, and makes it easier to scan (Visual Geography).
- Junior: Checks for the happy path (
if (exists)). - Pro: Checks for the unhappy path (
if (!exists)).
2. Law of Loudness
In the After example we never return false.
- If the ID is wrong →
Refund failed: Order 123 not found. - If the order is pending →
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 read the log.
3. Contextual Wrappers
Third‑party APIs usually throw generic errors like 500 Server Error. If we let that bubble up, we don’t know whether it came from 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’re 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 loudly and early is code that is easy to maintain.
Be paranoid. Be loud. Be professional.
Stop writing code that silently fails.
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.
