Back to Exception Best Practices
Q3Question 3 of 4

What does throw early, catch late mean?

Key Concepts
  • Throw early means validate inputs and fail immediately at the service entry point with a clear specific exception
  • Catch late means handle exceptions at the boundary layer that has enough context to decide the response
  • Services throw domain exceptions and do not catch internally, controllers catch via @ControllerAdvice and map to HTTP responses
  • Throwing late means a null value travels through 5 methods before causing an NPE deep in the code with a misleading stack trace
  • Catching early means the service catches and returns null, leaving the controller with no idea what went wrong
  • Clean layering: Repository throws DataAccessException, Service wraps in domain exception, Controller maps to HTTP response

Definition

Throw early means validate and fail fast at the service entry point, while catch late means handle exceptions at the boundary layer that has enough context to decide the appropriate response.
  • Throw early = validate inputs at the method entry point and fail immediately with a clear, specific exception. Do not let bad data travel through 5 method calls before failing deep in the stack.
  • Catch late = do not catch in the service or repository layer. Let exceptions propagate up to the boundary layer (controller, message listener, batch scheduler) that has enough context to decide the response.
  • Services throw domain exceptions. Controllers catch via @ControllerAdvice and map to HTTP responses. Kafka listeners catch and send to DLQ. Batch processors catch, log, and skip the record.

Throw early: the teller at National Bank of India checks Priya's withdrawal form immediately. "Amount is negative? Reject now." Do not pass the form to the vault, the accountant, and the manager, only for the manager to say "this form is wrong." Catch late: the teller does not decide what to tell Priya about a system error. The branch manager (controller) decides: "Tell her we are experiencing technical difficulties."

Understanding the Concepts

What does throw early mean?

Throw early means validating inputs at the service entry point and failing immediately with a clear exception. This produces clean stack traces and meaningful error messages.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.Objects;
import java.util.Optional;

public class ThrowEarlyDemo {

    // WRONG: throw late. Null travels deep before failing.
    public static void wrongThrowLate(String tradeId) {
        // tradeId could be null. No check here.
        String normalized = tradeId.trim();  // NPE here if null!
        System.out.println("Normalized: " + normalized);
    }

    // CORRECT: throw early. Validate at entry.
    public static void correctThrowEarly(String tradeId) {
        Objects.requireNonNull(tradeId, "tradeId must not be null");
        if (tradeId.isBlank()) {
            throw new IllegalArgumentException("tradeId must not be blank");
        }
        String normalized = tradeId.trim();
        System.out.println("Normalized: " + normalized);
    }

    public static void main(String[] args) {
        try {
            correctThrowEarly("  TRD-001  ");
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        }

        try {
            correctThrowEarly(null);
        } catch (NullPointerException e) {
            System.out.println("Caught early: " + e.getMessage());
        }
    }
}
TEXT
Normalized: TRD-001
Caught early: tradeId must not be null

What does catch late mean?

Catch late means do not catch in the service layer. Let exceptions propagate to the boundary (controller, listener) that has the context to decide the response.

JAVA
package com.bank.demo;

public class CatchLateDemo {

    // WRONG: service catches and returns null
    static String wrongCatchEarly(String tradeId) {
        try {
            if (tradeId.equals("BAD")) throw new RuntimeException("Not found");
            return "SETTLED";
        } catch (RuntimeException e) {
            return null;  // Caller has NO idea why it failed
        }
    }

    // CORRECT: service throws, boundary catches
    static String correctThrowUp(String tradeId) {
        if (tradeId.equals("BAD")) {
            throw new RuntimeException("Trade not found: " + tradeId);
        }
        return "SETTLED";
    }

    public static void main(String[] args) {
        // Wrong: controller gets null, no context
        String result1 = wrongCatchEarly("BAD");
        System.out.println("Wrong: result is " + result1 + " (why? no idea)\n");

        // Correct: controller catches with full context
        try {
            String result2 = correctThrowUp("BAD");
        } catch (RuntimeException e) {
            System.out.println("Correct: controller catches - " + e.getMessage());
            System.out.println("Can return: 404 NOT_FOUND with meaningful message");
        }
    }
}
TEXT
Wrong: result is null (why? no idea)

Correct: controller catches - Trade not found: BAD
Can return: 404 NOT_FOUND with meaningful message

How does this create clean layering?

Each layer has a clear responsibility: validate, throw, or catch.

JAVA
package com.bank.demo;

public class CleanLayeringDemo {

    public static void main(String[] args) {
        System.out.println("Clean Exception Layering:");
        System.out.println();
        System.out.println("  Repository layer:");
        System.out.println("    - Throws DataAccessException (Spring wraps JDBC)");
        System.out.println("    - Does NOT catch");
        System.out.println();
        System.out.println("  Service layer:");
        System.out.println("    - Validates early (throw IllegalArgumentException)");
        System.out.println("    - Wraps in domain exception (SettlementException)");
        System.out.println("    - Does NOT catch and return null");
        System.out.println();
        System.out.println("  Controller layer:");
        System.out.println("    - ZERO try-catch");
        System.out.println("    - @ControllerAdvice catches late");
        System.out.println("    - Maps to HTTP: 404, 400, 500");
    }
}
TEXT
Clean Exception Layering:

  Repository layer:
    - Throws DataAccessException (Spring wraps JDBC)
    - Does NOT catch

  Service layer:
    - Validates early (throw IllegalArgumentException)
    - Wraps in domain exception (SettlementException)
    - Does NOT catch and return null

  Controller layer:
    - ZERO try-catch
    - @ControllerAdvice catches late
    - Maps to HTTP: 404, 400, 500

What should a senior engineer know?

DIAGRAM
- **Kafka/SQS listeners.** These are "catch late" boundaries. Catch Exception, decide: retry, send to DLQ, or skip with logging.
- **Batch processors.** Catch per-record, log, accumulate errors, continue processing. Report all failures at the end.
- **Optional.orElseThrow().** The modern "throw early" pattern: `repo.findById(id).orElseThrow(() -> new NotFoundException(id))`.

What are common interview follow-up questions?

  • What is the anti-pattern of catching too early? (Service catches and returns null/error code. Controller has no context.)
  • What is the anti-pattern of throwing too late? (Null propagates deep, NPE in the wrong place with misleading trace.)
  • Where should you catch in a REST API? (In @ControllerAdvice, not in the controller method itself.)

Visual Overview

DIAGRAM
  THROW EARLY:                    CATCH LATE:
  ┌────────────────────────┐     ┌────────────────────────┐
  │ Service entry point     │     │ Controller / Boundary   │
  │ if (id == null)         │     │ @ControllerAdvice       │
  │   throw IAE("id null") │     │ catches domain exception│
  │ if (!exists(id))        │     │ maps to HTTP response   │
  │   throw NotFound(id)   │     │ 404 / 400 / 500         │
  └────────────────────────┘     └────────────────────────┘
           │                               ▲
           │   exception propagates        │
           └───────────────────────────────┘

Key Takeaway

"Throw early, catch late" is the foundational exception handling principle. Throw early means validating inputs at the service entry point and failing immediately with a clear, specific exception. Do not let a null tradeId travel through 5 methods before causing a NullPointerException deep in the repository. Use Objects.requireNonNull() and Optional.orElseThrow() at the boundary.

Catch late means not catching in the service or repository layer. Let exceptions propagate to the boundary that has the context to decide the response: the REST controller maps to HTTP status codes via @ControllerAdvice, the Kafka listener sends to a dead letter queue, and the batch scheduler logs and skips the record.

This creates clean layering at National Bank of India: the repository throws DataAccessException, the service validates early and wraps in domain exceptions, and the controller catches late via the global handler. Each layer has one job. The anti-pattern is catching too early (service catches and returns null) or throwing too late (null propagates deep with a misleading stack trace).

Remember one sentence: validate and throw at the service entry point, and catch at the boundary that has the context to decide the response.

---

Share
Use ← → arrow keys to navigate