Skip to main content
Newv0.14.1: C# + C/C++ support.Read the changelog →
← Blog
Pattern Guide3 min read

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.