Chaining, rethrow, custom hierarchies & logging
Wrap a low-level cause, design an exception family, and log a fault before re-throwing it.
Exception chaining: keep the original cause
When you catch a low-level exception and respond by throwing a more meaningful one, you must not *lose* the original — its stack trace is the evidence of what really happened. Java preserves it through the cause: pass the caught exception as the second constructor argument, or call initCause():
try {
loadEvents(file);
} catch (IOException e) {
throw new ControllerException("could not start: " + file, e); // e is the cause
}
The new ControllerException now carries the IOException as its cause; getCause() returns it, and a printed stack trace shows both, joined by Caused by:. This is wrap-and-rethrow: translate a low-level failure into your domain's vocabulary without discarding the diagnostic trail. For chaining to work, your exception needs a constructor that accepts a cause and forwards it to super.
Designing a custom exception hierarchy
Real systems group related faults under a common base so a caller can catch the *whole family* with one handler. The Greenhouse fault types form such a family:
public class ControllerException extends Exception {
public ControllerException(String why) { super(why); }
public ControllerException(String why, Throwable cause) { super(why, cause); }
}
public class WindowMalfunction extends ControllerException {
public WindowMalfunction(String why) { super(why); }
}
public class PowerOut extends ControllerException {
public PowerOut(String why) { super(why); }
}
Now catch (ControllerException e) handles a WindowMalfunction or a PowerOut *or* a plain ControllerException — because catch matches the declared type and all its subtypes. A handler that needs to distinguish can add narrower catch clauses *above* the broad one (order matters: most specific first, or the broad clause shadows them and the compiler rejects the unreachable narrower catch).
Logging then re-throwing — the project's shutdown path
The Greenhouse controller's top level catches a fault, logs it with a timestamp and reason to error.log, and then re-throws (or proceeds to an emergency shutdown). The logging happens in the handler *before* recovery:
try {
controller.run();
} catch (ControllerException e) {
try (PrintWriter log =
new PrintWriter(new FileWriter("error.log", true))) { // true = append
log.println(System.currentTimeMillis() + ": " + e.getMessage());
} catch (IOException io) {
System.err.println("could not write log: " + io.getMessage());
}
shutdown(); // emergency cleanup
}
Note the nested try-with-resources for the log writer (opened in append mode so each fault adds a line rather than truncating the file), and the inner catch so a logging failure can't mask the original fault. This is the shape the capstone expects: observe the fault, persist it, then recover — never silently swallow it.