Flawed error handling begins after a failure has been intercepted: code must decide what the failure means and what happens next. Harmful strategies hide a problem, leak low-level details, use an error type that misstates the failure, or choose recovery without regard to the caller or system boundary. Treat failure handling as part of the API and its readable story, not as cleanup around the happy path. section 7.2 section 7.2.4
An empty catch block is the clearest failure: it silently discards information and assumes a condition can never occur. Future changes can invalidate that assumption. Static analysis can flag empty catches, but a genuinely harmless failure still deserves a comment explaining why it is harmless—for example, deleting an already-removed temporary file or closing an already-closed connection. section 7.2.4
try {
int parsedNumber = Integer.parseInt(data);
} catch (NumberFormatException exception) {
}
This catch discards a parsing failure. No caller can distinguish invalid input from a successful path that simply produced no useful result. section 7.2.4
Repeatedly ignored exceptions can reveal a design problem rather than a local catch-block problem. Ask whether the API throws a meaningful, actionable exception at all; when failure is an expected outcome, a return type such as Optional, a boolean, or an explicit result can communicate it more directly. section 7.2.4
Choose handling by making the failure explicit to the right layer:
- Wrap a low-level parsing exception in a domain-specific exception when callers need a processing-level failure and the cause remains useful. section 7.2.4
- Avoid generic exceptions for validation; name the business-relevant invalid condition instead. section 7.2.4
- Return an explicit valid-or-invalid result when invalid input is an expected result that callers should handle as normal control flow. section 7.2.4
- Review the successful path, failure source, recipient, system boundary, and whether recovery, retry, fallback, propagation, or fail-fast behavior fits the situation. section 7.2.4
Preserve context without exposing the wrong abstraction
Parsing data may fail because the input is malformed, but callers of a data-processing operation need to understand that operation’s failure. A DataProcessingException can carry the input context, a clear message, and the original NumberFormatException as its cause. This replaces a technical detail at the boundary with a meaningful application-level signal while retaining diagnostic context. section 7.2.4
int dataToTransform = 0;
try {
dataToTransform = Integer.parseInt(data);
} catch (NumberFormatException exception) {
throw new DataProcessingException(data, "Processing error", exception);
}
The wrapper translates NumberFormatException into DataProcessingException while preserving the original exception as the cause. section 7.2.4
Model validation as a result
An eight-digit order ID can be represented as either OrderIdValid or OrderIdInvalid with a reason. A caller can then process the valid ID or alert on the invalid one through an exhaustive-looking branch, rather than treating ordinary validation feedback as an unexpected technical exception. section 7.2.4
public OrderIdValidationResult validateOrderId(String orderId) {
if (!orderId.matches("\\d{8}")) {
return new OrderIdInvalid(orderId,
"Order ID must be exactly 8 digits.");
}
return new OrderIdValid(orderId);
}
The validator returns either a valid order ID or an invalid result carrying the rejected value and reason. section 7.2.4
A useful final rule is: never make failure disappear by default. Classify it, preserve the public contract and architectural boundary, avoid sensitive details, and justify any fallback that changes behavior. If the right response is intentionally no action, record that intent where a future reader can verify it. section 7.2.4
