Clean Code Chapter 7: Error Handling

Principles

Error handling is important enough to deserve its own attention, and important enough not to clutter business logic. If failure handling obscures what the code is supposed to do on the happy path, the design is wrong.

Prefer exceptions to return codes

Return codes and status flags force every caller to check immediately, nesting the real work inside error ladders. Exceptions separate the success story from recovery.

Write try / catch / finally first

When a block can fail, start by defining the scope: what must happen on success, what recovery looks like, and what always releases (files, locks, connections). That scope teaches callers what to expect.

Provide context

Bare exceptions waste time. Include what operation failed, which inputs mattered, and enough state to diagnose without a debugger séance.

Define exceptions by caller need

Classify errors by how callers must respond (retryable network fault vs permanent validation failure), not by every internal throw site.

Don’t return or pass null

null pushes crashes downstream and forces noisy checks. Prefer empty collections, Optional, special-case objects, or exceptions at the boundary.

Wrap third-party failures

At module edges, translate foreign exceptions into your types so the rest of the system does not depend on vendor error models.

Example

Examples below are in Java.

Error codes bury the story:

public void sendShutDown() {
  DeviceHandle handle = getHandle(DEV1);
  if (handle != DeviceHandle.INVALID) {
    DeviceRecord record = retrieveDeviceRecord(handle);
    if (record.getStatus() != DEVICE_SUSPENDED) {
      pauseDevice(handle);
      clearDeviceWorkQueue(handle);
      closeDevice(handle);
    } else {
      logger.log("Device suspended");
    }
  } else {
    logger.log("Invalid handle");
  }
}

Exceptions keep the happy path linear:

public void sendShutDown() {
  try {
    tryToShutDown();
  } catch (DeviceShutDownError error) {
    logger.log(error);
  }
}

private void tryToShutDown() {
  DeviceHandle handle = getHandle(DEV1);
  DeviceRecord record = retrieveDeviceRecord(handle);
  pauseDevice(handle);
  clearDeviceWorkQueue(handle);
  closeDevice(handle);
}

Prefer empty results over null:

public List<Employee> getEmployees() {
  if (noEmployees()) {
    return List.of();
  }
  return employees;
}

Bottom line

Keep the happy path readable. Put context in exceptions. Avoid returning null when an empty value or a real error works better.