Loading learning content...
There is an anti-pattern so dangerous, so insidious, that it has earned its own name in the software engineering lexicon: exception swallowing. It occurs when a developer catches an exception and then... does nothing. The exception vanishes without a trace, no log, no rethrow, no indication that anything went wrong.
Exception swallowing is the software equivalent of a smoke detector with dead batteries. The detector exists, but when fire breaks out, you never know until it's too late. Systems that swallow exceptions appear to work correctly—until they catastrophically fail in ways that nobody can diagnose.
Exception swallowing is not merely a code smell or stylistic preference. It is a critical defect that renders your system unobservable, undebuggable, and unreliable. Every swallowed exception is a potential production incident waiting to happen—with no trail to follow when it does.
By the end of this page, you will understand why exception swallowing is so dangerous, learn to identify its various manifestations, and master the techniques for ensuring that every exception produces a visible, actionable signal.
Exception swallowing takes many forms, from blatantly obvious to deceptively subtle. Let's examine the spectrum:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// THE SPECTRUM OF EXCEPTION SWALLOWING // ❌ LEVEL 1: THE EMPTY CATCH (Blatantly Obvious)// The classic, unmistakable anti-patterntry { performCriticalOperation();} catch (Exception e) { // Nothing here. Absolutely nothing.} // ❌ LEVEL 2: THE TODO GRAVEYARD (Developer Intention)// "I'll fix this later" - narrator: they didn'ttry { saveUserData(data);} catch (IOException e) { // TODO: handle this properly} // ❌ LEVEL 3: THE SILENT PRINT (False Observability)// Looks like logging but goes nowhere visibletry { processPayment(order);} catch (PaymentException e) { System.out.println("Error: " + e.getMessage()); // Lost to stdout} // ❌ LEVEL 4: THE MISLEADING RETURN (Hidden Control Flow)// Exception is caught but program continues with wrong datapublic User findUser(String id) { try { return userRepository.findById(id); } catch (DataAccessException e) { return null; // Caller has no idea something went wrong }} // ❌ LEVEL 5: THE FALSE LOG (Inadequate Context)// Logs but loses critical diagnostic informationtry { connectToDatabase();} catch (SQLException e) { logger.error("Database error occurred"); // No exception, no stack trace} // ❌ LEVEL 6: THE RETHROW DISGUISER (Information Destruction)// Catches one exception, throws another, loses causetry { parseConfiguration(config);} catch (ConfigParseException e) { throw new RuntimeException("Config error"); // Original cause lost!}Every level on this spectrum represents a failure mode. The difference is only in how quickly you'll discover the problem. Empty catches fail silently for years. Inadequate logging fails when you need to debug a production incident at 3 AM.
Understanding why exception swallowing happens is essential to eliminating it. Nobody writes empty catch blocks with malicious intent. They do it for seemingly rational reasons that are actually flawed:
| Rationalization | Reality | Proper Response |
|---|---|---|
| "This exception can't actually happen" | If it truly can't happen, the JVM/runtime won't throw it. If the compiler requires handling, there's a reason. | If genuinely impossible, throw AssertionError/unreachable with explanation |
| "I'll handle it properly later" | Technical debt accrues interest. That 'later' becomes 'never' 95% of the time. | Handle it now, even if basic. Use a TODO with ticket reference. |
| "The exception is not important" | You don't know what's important until something breaks. All exceptions are signals. | At minimum, log at DEBUG level. Let observability systems decide importance. |
| "I don't want to clutter the logs" | Cluttered logs are fixable. Missing logs during incidents are catastrophic. | Use structured logging with levels. Filter later, not at write time. |
| "The calling code can't handle errors anyway" | Then propagate up until you reach code that can. That's what exception propagation is for. | Let exceptions bubble. Handle at system boundaries. |
| "It's just a checked exception the API forces me to handle" | The API designer had a reason. Checked exceptions signal recoverable conditions. | Wrap in unchecked if truly unrecoverable, but include original cause. |
The Core Fallacy: Every rationalization assumes the developer knows, at development time, which exceptions are important. But exceptions are signals about runtime conditions. You cannot know which signals matter until you observe the system in production under real load with real data.
Treat every exception as potentially important diagnostic information. The cost of logging an unimportant exception is negligible. The cost of missing a critical exception is a production incident with no debugging trail.
Exception swallowing doesn't just hide immediate errors—it creates cascading failures that manifest far from the original problem. Let's trace through a realistic scenario:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
/** * A realistic scenario showing how swallowed exceptions cascade * into production nightmares. */ // STEP 1: A developer swallows an exception "because it rarely happens"public class UserPreferencesService { public void updatePreferences(String userId, Preferences prefs) { try { preferencesCache.update(userId, prefs); // Step 1 database.writePreferences(userId, prefs); // Step 2 } catch (CacheUpdateException e) { // "Cache failures are transient, it'll fix itself" // ❌ SWALLOWED - no log, no metrics, nothing } }} // STEP 2: Hours later, the cache and database are out of sync// Cache still has old preferences, database has new ones // STEP 3: Another service reads from cache (because it's fast)public class RecommendationService { public List<Product> getRecommendations(String userId) { Preferences prefs = preferencesCache.get(userId); // Gets STALE data return generateRecommendations(prefs); // Wrong recommendations }} // STEP 4: User sees wrong recommendations// User reports: "Your recommendations are terrible"// Support: "Sorry, we'll look into it" // STEP 5: Engineering investigates// - Recommendation algorithm looks correct ✓// - User preferences in database look correct ✓// - Cache data... wait, where are the cache logs?// - No exception logs, no metrics, NO TRAIL // STEP 6: After 3 days of investigation// "We have no idea what happened. We'll monitor it." // STEP 7: Problem continues for months// User churn increases 15%// Nobody connects it to that one swallowed exceptionThe most insidious aspect of exception swallowing is that you don't know it's happening. The system appears healthy. Metrics look normal. Then one day, you discover that for the past 6 months, 5% of user data updates silently failed—and you have no way to identify which ones.
To prevent exception swallowing, adopt a simple contract: Every caught exception MUST result in at least one of these four actions:
Notice what's NOT on this list: Ignoring, suppressing, or silently continuing. There is no fifth option.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
/** * Demonstration of the four valid exception responses. */public class ExceptionContractExamples { // RESPONSE 1: LOG WITH FULL CONTEXT // Use when: Exception is informational, operation can continue public void logExample() { try { refreshCacheInBackground(); } catch (CacheRefreshException e) { logger.warn("Cache refresh failed, using stale data. " + "CacheId={}, LastRefresh={}, Reason={}", cacheId, lastRefreshTime, e.getMessage(), e); // Operation continues with stale cache metrics.increment("cache.refresh.failures"); } } // RESPONSE 2: RETHROW (optionally wrapped) // Use when: Current method cannot handle, caller might public User loadUser(String userId) throws DataAccessException { try { return database.query("SELECT * FROM users WHERE id = ?", userId); } catch (SQLException e) { // Wrap in domain exception, preserve original cause throw new DataAccessException( "Failed to load user: " + userId, e); } } // RESPONSE 3: RETURN ERROR INDICATOR // Use when: Error is expected, caller needs structured response public Result<Order, OrderError> createOrder(OrderRequest request) { try { Order order = orderService.create(request); return Result.success(order); } catch (InsufficientInventoryException e) { logger.info("Order failed due to inventory: {} for SKUs {}", request.getId(), e.getAffectedSkus()); return Result.failure( new OrderError(OrderErrorCode.INSUFFICIENT_INVENTORY, e.getAffectedSkus())); } } // RESPONSE 4: SELF-HEAL AND LOG RECOVERY // Use when: Exception is recoverable through alternative action public Connection getConnection() throws ServiceUnavailableException { try { return primaryPool.acquire(); } catch (PoolExhaustedException e) { logger.warn("Primary pool exhausted, failing over to secondary. " + "PoolSize={}, WaitingRequests={}", primaryPool.size(), primaryPool.waiting(), e); metrics.increment("pool.failover.count"); try { return secondaryPool.acquire(); } catch (PoolExhaustedException e2) { // Secondary also exhausted - now we truly can't handle throw new ServiceUnavailableException( "All connection pools exhausted", e2); } } } // ❌ NEVER: The empty/silent catch public void neverDoThis() { try { importantOperation(); } catch (Exception e) { // This is NEVER acceptable } }}If logging is one of the four valid responses, then logging correctly is essential. Bad logging is almost as harmful as no logging.
logger.error("Error")logger.error(e.getMessage())logger.error("Error: " + e)System.out.println(e)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
/** * Demonstration of proper exception logging patterns. */public class LoggingPatterns { // ❌ BAD: No useful information public void badLogging1() { try { operation(); } catch (Exception e) { logger.error("Error"); // ❌ What error? Where? Why? } } // ❌ BAD: Loses stack trace public void badLogging2() { try { operation(); } catch (Exception e) { logger.error("Error: " + e.getMessage()); // ❌ No stack trace! } } // ❌ BAD: Wrong level public void badLogging3() { try { criticalPaymentProcess(); } catch (PaymentException e) { logger.debug("Payment failed", e); // ❌ DEBUG?! This is critical! } } // ✅ GOOD: Complete context, structured logging public void goodLogging() { String orderId = order.getId(); String userId = user.getId(); BigDecimal amount = order.getTotal(); try { paymentGateway.charge(order); } catch (PaymentDeclinedException e) { // Expected business error - INFO level logger.info("Payment declined for order. OrderId={}, UserId={}, Amount={}, " + "DeclineCode={}, TraceId={}", orderId, userId, amount, e.getDeclineCode(), MDC.get("traceId"), e); } catch (PaymentGatewayException e) { // Infrastructure failure - ERROR level logger.error("Payment gateway failure. OrderId={}, UserId={}, Amount={}, " + "Gateway={}, TraceId={}", orderId, userId, amount, gatewayName, MDC.get("traceId"), e); // Note: exception is LAST parameter // Also emit metric for alerting metrics.increment("payment.gateway.errors", Tags.of("gateway", gatewayName, "error_type", e.getClass().getSimpleName())); } } // ✅ GOOD: Structured logging with log aggregation compatibility @WithSpan // OpenTelemetry trace annotation public void structuredLogging(OrderRequest request) { try { processOrder(request); } catch (OrderProcessingException e) { // Structured logging for ELK/Splunk/CloudWatch logger.error("Order processing failed", StructuredArguments.kv("orderId", request.getId()), StructuredArguments.kv("customerId", request.getCustomerId()), StructuredArguments.kv("productCount", request.getItems().size()), StructuredArguments.kv("failureReason", e.getReason()), StructuredArguments.kv("traceId", Span.current().getSpanContext().getTraceId()), e); // Exception always last } }}ERROR: Infrastructure failures, unexpected exceptions, conditions requiring investigation. WARN: Expected problems handled gracefully (retries, fallbacks). INFO: Business events including expected rejections (payment declined, validation failed). DEBUG: Detailed diagnostic info not needed in production.
Exception swallowing often slips into codebases gradually. Establishing automated detection and team practices is essential.
123456789101112131415161718192021222324252627282930313233343536373839404142
# Static Analysis Configuration Examples # Java - SpotBugs (FindBugs successor)# spotbugs-exclude.xml - DO NOT exclude these detectors!# REC_CATCH_EXCEPTION: method catches Exception# DE_MIGHT_IGNORE: dead exception - catch/return with no use # Java - Error Prone (Google's static analysis)# build.gradlecompileJava { options.errorprone.error("EmptyCatch") options.errorprone.error("CatchAndPrintStackTrace")} # C# - .editorconfig rules[*.cs]# CA2200: Rethrow to preserve stack detailsdotnet_diagnostic.CA2200.severity = error# CA1031: Do not catch general exception typesdotnet_diagnostic.CA1031.severity = warning # ESLint for TypeScript# .eslintrc.js{ rules: { "no-empty": ["error", { "allowEmptyCatch": false }], "@typescript-eslint/no-empty-function": "error", }} # Python - pylint# .pylintrc[MESSAGES CONTROL]enable=bare-except,broad-except # SonarQube rules (multi-language)# These rules should be enabled and set to "Blocker":# - java:S108 - Nested blocks of code should not be empty# - java:S2142 - InterruptedException should not be ignored# - java:S1166 - Exception handlers should preserve original exceptions# - cs:S108 - Nested blocks should not be empty# - ts:S108 - Nested blocks should not be emptycatch (Exception with no following log/throw statements.Adopt a zero-tolerance policy for empty catch blocks. If code review finds one, it's a blocking comment. If it slips into production, create a bug ticket immediately. The cultural norm should be: swallowed exceptions are always bugs.
Exception swallowing is one of the most destructive anti-patterns in software development. Let's consolidate the key lessons:
The next page explores exception wrapping and chaining—the techniques for enriching exceptions with context as they propagate through system layers while preserving the original diagnostic information.