Low-level designCore6 min
Errors and exceptions
An error that's caught and ignored doesn't go away: it turns into wrong data. Decide what kind of failure each one is, handle it where something useful can be done, and let the rest fail loudly.
A shop imports its prices
Each line is a product with a price; a helper turns "8.50" into cents.
1 · The situation
A shop imports its prices
A small online shop updates prices from a spreadsheet exported as lines of sku;name;price. A helper, Prices.cents(), turns "8.50" into 850 cents and throws on anything else. The importer's author decided one bad price shouldn't stop the whole import, so it catches the error and carries on:
class PriceImporter {
private final List<Product> products = new ArrayList<>();
void importAll(List<String> lines) {
for (String line : lines.subList(1, lines.size())) {
String[] f = line.split(";", -1);
int price;
try {
price = Prices.cents(f[2]);
} catch (NumberFormatException e) {
price = 0;
}
products.add(new Product(f[0], f[1], price));
}
}
List<Product> products() {
return products;
}
}Drawn from the code: the importer keeps the products it creates and uses the Prices helper.
2 · The change
Two products, free of charge
This week's file has a price typed with a comma, 12,50, and a row with the price left blank:
Imported 4 products Mug: $8.50 Scarf: $0.00 Lamp: $0.00 Bowl: $14.00
"Imported 4 products", two of them at $0.00. Nothing said anything was wrong.
The import "succeeded", and the shop now sells a scarf and a lamp for nothing. Catching an error and carrying on as if nothing happened is called swallowing it. The error didn't disappear; it became wrong data, found later by a customer, with no record of where it came from.
The author's instinct was right: one bad row shouldn't stop the rest. What went wrong is that the failure was hidden instead of handled.
3 · The fix
Name the failure, handle it where it can be, fail the rest loudly
A bad line gets its own error type: BadLineException in Java, BadLine in the other languages, with a message that says what's wrong. Parsing a line throws it instead of inventing a price.
class PriceImporter {
static final String HEADER = "sku;name;price";
private final List<Product> products = new ArrayList<>();
private final List<String> rejected = new ArrayList<>();
// A missing header fails the whole import; a bad line is set aside and reported.
void importAll(List<String> lines) {
if (lines.isEmpty() || !lines.get(0).equals(HEADER)) {
throw new IllegalArgumentException("the file has no header");
}
for (int i = 1; i < lines.size(); i++) {
try {
products.add(parse(lines.get(i)));
} catch (BadLineException e) {
rejected.add("Line " + (i + 1) + ": " + e.getMessage());
}
}
}
private Product parse(String line) throws BadLineException {
String[] f = line.split(";", -1);
if (f.length != 3) throw new BadLineException("expected 3 fields");
if (f[2].isEmpty()) throw new BadLineException("missing price");
try {
return new Product(f[0], f[1], Prices.cents(f[2]));
} catch (NumberFormatException e) {
throw new BadLineException(e.getMessage(), e);
}
}
List<Product> products() {
return products;
}
List<String> rejected() {
return rejected;
}
}Imported 2 products, rejected 2 Mug: $8.50 Bowl: $14.00 Line 3: "12,50" is not a price Line 4: missing price Import failed: the file has no header
Two products imported, two lines rejected with their reasons, and an empty file refused outright.
4 · Kinds of failure
Which kind of failure is it?
Kind | Example | Usual handling |
|---|---|---|
Expected bad input | A price typed as | Report it with a clear message; skip it or ask again |
Trouble outside the program | The file is missing; the network is down | Let it reach a layer that can retry, or tell the user |
A bug | A null where none should be; an index out of range | Fail at once; fix the code, don't catch it |
Failing early is a design choice called fail fast: report a problem at the point it's detected, instead of carrying on in a state that will cause a stranger failure later. The constructors on the page Classes, objects and state do the same: an invalid gift card is refused at once, not spent later.
5 · Exceptions or results
Throw, or return the failure?
An exception jumps out of the code that hit the problem, up through its callers, until some code catches it. A result value is returned like any other value, holding either the outcome or the error, and the caller must look inside it. Both are sound; languages lean different ways:
Language | The usual way | Notes |
|---|---|---|
Java | Exceptions; | Checked exceptions are part of a method's signature |
C++ | Exceptions, or a value holding a result or an error | C++23 adds |
Python | Exceptions: try it, and catch the specific error | The language's own style is "easier to ask forgiveness than permission" |
JavaScript | Exceptions, and rejected promises in async code | Pass the original as |
Java adds a twist: a checked exception, like BadLineException here, must be caught or declared by every caller, so the compiler won't let anyone forget it. The usual guideline: make it checked if callers can reasonably recover from it, and unchecked (a RuntimeException) if they can't, as with bugs.
6 · Where to catch
Rules of thumb
- Catch where you can act. The import loop can skip and report a line;
Prices.cents()can't, so it throws. - Catch the specific error. Catching every error also catches bugs, and hides them.
- Keep the cause. When you wrap one error in another, carry the original along:
throw new X(..., e)in Java,std::throw_with_nested(X(...))in C++,new X(msg, { cause: e })in JavaScript,raise X(...) from ein Python. - Never catch and do nothing. If ignoring really is right, say why in a comment, and still log it.
- Say what went wrong, in the user's terms.
"12,50" is not a pricehelps the person fixing the spreadsheet; a stack trace doesn't.
The error travels up to the importer, which can do something with it, and goes no further.
7 · In practice
A coin the machine doesn't take
In the vending-machine problem, a foreign coin is expected bad input: refuse it with a message and hand it back, rather than ignoring it or counting it as zero. Running out of a snack is a normal outcome, not an error at all.
Design the vending machine and decide what each failure is.
Check yourself
4 questions
Takeaways
Remember this
- Catching an error and carrying on silently turns it into wrong data.
- Name each failure with its own error type and a message in the user's terms.
- Catch where something useful can be done; let the rest propagate.
- Fail fast on anything that makes the rest meaningless, like a missing header or an invalid object.
- Exceptions and result values both work; follow the language's usual style, and always keep the cause.