Refactoring techniqueSimplifying Method Calls
Replace Error Code with Exception
Throw a typed exception instead of returning a sentinel such as -1 or false, so failures cannot be silently ignored by callers.
Problem
A method signals failure with a special return value: -1, null, false or a status string. Every caller must remember to check it. Callers that forget pass the sentinel on as if it were real data, and the error surfaces far away.
Solution
Throw an exception that names the failure, and remove the error code from the return type. Callers that can handle the failure catch it; the rest let it propagate.
Before
// Returns new balance, or -1 if insufficient funds, -2 if the account is frozen
function withdraw(account: Account, amountCents: number): number {
if (account.frozen) return -2;
if (account.balanceCents < amountCents) return -1;
account.balanceCents -= amountCents;
return account.balanceCents;
}
const result = withdraw(acct, 5000);
if (result === -1) return res.status(402).send("Insufficient funds");
if (result === -2) return res.status(423).send("Account frozen");
res.json({ balance: result });After
class InsufficientFundsError extends Error {}
class AccountFrozenError extends Error {}
function withdraw(account: Account, amountCents: number): number {
if (account.frozen) throw new AccountFrozenError(account.id);
if (account.balanceCents < amountCents) throw new InsufficientFundsError(account.id);
account.balanceCents -= amountCents;
return account.balanceCents;
}
try {
res.json({ balance: withdraw(acct, 5000) });
} catch (err) {
if (err instanceof InsufficientFundsError) return res.status(402).send("Insufficient funds");
if (err instanceof AccountFrozenError) return res.status(423).send("Account frozen");
throw err;
}Why
- A forgotten check no longer turns
-1into a balance shown to the user. - The return type means only success, which simplifies every caller that does not handle the error itself.
- Error handling can live at one boundary (an HTTP error middleware) instead of at every call.
Steps
- Decide which failures are exceptional. Create one exception class per failure that callers handle differently.
- Find every caller and check how it handles each code. Add
try/catcharound callers that react to the code, mapping each exception to the old reaction. Keep the code checks too for now. Run the tests. - Change the method to throw instead of returning the code. Run the tests.
- Remove the now unreachable code checks from callers. Run the tests.
- Update the method's documentation and return type.
Watch out
- Do not use exceptions for expected outcomes that callers check every time, like "user not found" in a lookup. Return
undefined, anOption, or aResulttype instead. - In Go, Rust and other languages built around error values, returning an error is the idiom. Apply the spirit, typed errors instead of magic numbers, without throwing.
- Exceptions are slower than returns in hot loops. Keep error codes or a precondition check there; see Replace Exception with Test.
- Async code must
awaitthe call inside thetry, or the rejection escapes. - The inverse direction is Replace Exception with Test, for failures callers can predict.
Related
- Inverse direction: Replace Exception with Test.
- Removes Switch Statements over return codes and Duplicate Code in repeated checks.
- For programmer errors rather than runtime failures, use Introduce Assertion.
Agent checklist
- The failure is exceptional for most callers, not a routine branch.
- Each distinct handling path has its own exception type.
- Every caller that reacted to a code now catches the matching exception before the method was changed.
- Catch blocks rethrow unknown errors.
Treats these smells
See also
- Replace Exception with TestCheck a predictable condition before the call instead of catching the exception it would throw, keeping exceptions for real failures.
- Introduce AssertionState an assumption the code already relies on as an explicit check that fails fast, instead of leaving it in a comment or in the reader's head.
- Separate Query from ModifierSplit a method that both returns a value and changes state into a side-effect-free query and a separate command.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.