Skip to content
aipatterns.dev

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

TypeScript
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

TypeScript
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 TypeError from a bug elsewhere is no longer mistaken for a missing price.
  • No exception objects or stack traces are built on a common path.

Steps

  1. Identify the condition that causes the exception in the expected case.
  2. Add a check for that condition before the risky call, returning what the catch block returned. Leave the try/catch in place. Run the tests.
  3. Add a temporary log or assertion in the catch block for the expected case. If tests show it is never reached, the check is complete.
  4. Remove the catch branch that handled the expected case. If nothing else is caught, remove the try. Run the tests.

Watch out

  • Do not replace a catch with 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.

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

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.