# Replace Exception with Test

> Check a predictable condition before the call instead of catching the exception it would throw, keeping exceptions for real failures.

- kind: technique
- category: simplifying-method-calls
- aliases: Replace Exception with Precheck
- canonical: https://aipatterns.dev/refactoring/techniques/replace-exception-with-test
- inspired-by: https://refactoring.guru/replace-exception-with-test

## 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

```ts
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

```ts
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](https://aipatterns.dev/refactoring/techniques/replace-error-code-with-exception.md).

## Related

- Inverse: [Replace Error Code with Exception](https://aipatterns.dev/refactoring/techniques/replace-error-code-with-exception.md).
- Shortens [Long Method](https://aipatterns.dev/refactoring/smells/long-method.md) by removing exception-handling noise.
- For repeated "missing" checks, consider [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md).

## 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.
