Swallowed Exceptions: How Silent Failures Reach Production
A catch block is not error handling merely because it compiles. It needs an explicit policy: recover, translate, retry, propagate, or terminate safely. Silence is the dangerous default.
Swallowed exceptions are serious without dramatic anecdotes or unsupported prevalence claims. The risk is concrete: the program detects a failure, suppresses it, and may continue from a state the caller mistakes for success.
CWE-390 defines the weakness precisely: software detects an error condition but takes no action. MITRE notes that ignored errors can leave a system in an unexpected state and alter execution logic.
The simplest failure
async function loadAccount(id) {
try {
return await api.getAccount(id);
} catch (error) {
// ignored
}
}The function now returns undefined on authentication failure, timeout, malformed response, rate limit, or programmer error. Callers cannot distinguish those failures from a deliberately absent account unless the contract explicitly allows it.
The visible symptom may appear far downstream: an empty dashboard, skipped write, stale cache, or secondary null error. The original cause has lost both control flow and context.
Log-and-continue can be just as silent
try {
await chargeCustomer(order);
} catch (error) {
console.error("Payment failed");
}
await markOrderAsPaid(order.id);A log line helps observability, but it does not repair control flow. If execution continues into a state that assumes success, the exception is functionally swallowed. Error handling must protect the program's state, not only produce text.
Five valid policies
Recover: restore a known safe state and continue with behavior the contract permits.
Translate: convert an infrastructure error into an explicit domain error or typed result the caller understands.
Retry: retry only transient operations, with bounds, backoff, idempotency, and cancellation.
Propagate: add useful context if needed and let the appropriate boundary decide.
Terminate safely: clean up resources, surface the failure, and stop when continuing would leave the system inconsistent.
A better implementation
async function loadAccount(id) {
try {
return await api.getAccount(id);
} catch (error) {
if (error instanceof AccountNotFoundError) {
return { ok: false, reason: "not_found" };
}
throw new AccountLoadError(id, { cause: error });
}
}One expected condition becomes an explicit result. Unexpected conditions retain the original cause and propagate. Whether to log depends on the application's observability design; repeatedly logging the same exception at every layer can create duplicate noise.
OWASP's error-handling guidance also distinguishes internal diagnostic detail from safe user-facing responses. Do not expose stack traces, tokens, query details, or secrets while trying to add context.
Review and test the failure path
- What exact exceptions can the protected operation raise?
- After the catch, is the system in a state the next line can safely use?
- Can the caller distinguish absence, rejection, timeout, and internal failure?
- Are resources released even when recovery or rethrow itself fails?
- Does a test force the dependency to fail and assert the visible outcome?
- Does observability preserve useful context without leaking sensitive data?
What aislop checks
The current rule catalog documents ai-slop/swallowed-exception for empty and log-only catches across supported languages, and ai-slop/silent-recovery for some paths that log or default and then continue. Those rules are intentionally narrower than a proof of correct error handling.
npx --yes aislop@latest scan
Use the diagnostic as a review prompt: should this layer recover, translate, retry, propagate, or stop? The answer comes from the function's contract and system design.
Sources
Frequently asked questions
What is a swallowed exception?
It is an error that code catches but neither handles meaningfully nor propagates. Empty catches and log-and-continue blocks are common examples, especially when callers proceed as though the operation succeeded.
Should every catch block rethrow the error?
No. A catch can recover, translate the error into a domain result, retry safely, return an explicit failure, or terminate cleanly. It should rethrow when the current layer cannot restore a known safe state.
Can static analysis detect swallowed exceptions?
Static analysis can detect patterns such as empty catches and some log-only or default-and-continue paths. It cannot determine every valid recovery policy, so findings still need context and tests.