Refactoring techniqueSimplifying Method Calls
Replace Exception with Test
Check a predictable condition before the call instead of catching the exception it would throw, keeping exceptions for real failures.
- Also known as
- Replace Exception with Precheck
Problem
Code calls something, catches the exception, and treats the catch block as a normal branch. The caller could have checked the condition first. Exceptions used as control flow hide the normal logic, are costly in loops, and swallow unrelated errors that share the same type.
Solution
Test for the condition before the call, and handle the expected case in a plain conditional. Keep the catch only for failures that cannot be predicted.
Before
function unitPrice(catalog: Map<string, Product>, sku: string, tier: PriceTier): number {
try {
return catalog.get(sku)!.prices[tier].amountCents;
} catch (err) {
if (err instanceof TypeError) {
// SKU missing or tier not priced: fall back to list price
return catalog.get(sku)?.listPriceCents ?? 0;
}
throw err;
}
}After
function unitPrice(catalog: Map<string, Product>, sku: string, tier: PriceTier): number {
const product = catalog.get(sku);
if (!product) return 0;
const tierPrice = product.prices[tier];
if (!tierPrice) return product.listPriceCents;
return tierPrice.amountCents;
}Why
- The expected cases read as ordinary logic, not as error recovery.
- A real
TypeErrorfrom a bug elsewhere is no longer mistaken for a missing price. - No exception objects or stack traces are built on a common path.
Steps
- Identify the condition that causes the exception in the expected case.
- Add a check for that condition before the risky call, returning what the
catchblock returned. Leave thetry/catchin place. Run the tests. - Add a temporary log or assertion in the
catchblock for the expected case. If tests show it is never reached, the check is complete. - Remove the
catchbranch that handled the expected case. If nothing else is caught, remove thetry. Run the tests.
Watch out
- Do not replace a
catchwith a check when the condition can change between check and use. File existence, record locks, network availability and unique-key inserts are race-prone. Try the operation and handle the error ("look before you leap" fails here). - Do not do this when checking is as expensive as doing, such as parsing a string to see whether it parses. Use a non-throwing variant (
safeParse,tryParse) instead. - Python's idiom favors trying first and catching (
EAFP) for many cases. Follow the language's convention where it differs. - The inverse technique is Replace Error Code with Exception.
Related
- Inverse: Replace Error Code with Exception.
- Shortens Long Method by removing exception-handling noise.
- For repeated "missing" checks, consider Introduce Null Object.
Agent checklist
- The caught exception covered a predictable, routine condition.
- The condition cannot change between the check and the call (no race).
- The check is cheaper than the operation it guards.
- Unexpected errors of the same type now propagate instead of being swallowed.
Treats these smells
See also
- Replace Error Code with ExceptionThrow a typed exception instead of returning a sentinel such as -1 or false, so failures cannot be silently ignored by callers.
- Introduce Null ObjectReplace repeated null checks for a missing collaborator with an object that implements the same interface and does the safe default.
- Decompose ConditionalMove a complex condition and each of its branches into well-named functions so the decision reads as intent instead of arithmetic.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.