> ## Content Index
> Fetch the complete content index at: https://www.codyssey.tech/llms.txt
> Use this file to discover other available public pages before exploring further.

# 💥 Fail Gracefully: Error Handling, Validation, and the Art of Useful Errors
- URL: https://www.codyssey.tech/fail-gracefully/
- Published: 2026-06-03T08:00:00.000Z
- Updated: 2026-06-10T06:25:09.000Z
- Description: Generic error responses tell your users nothing and your developers even less. This article introduces the CRAFT framework — sealed exception hierarchies, RFC 7807 ProblemDetail responses, multi-layer validation, and a GlobalExceptionHandler that turns failures into useful communication.
- Author: Robert Marcel Saveanu
- Tags: Architecture & Infra, Microservices, Spring Boot, #format-tutorial, #series-spring-boot, #syntax-highlight

📚 **Series Navigation:**  
← **Previous:** [Part 7 - Guarding the Gates](https://www.codyssey.tech/guarding-the-gates/)  
👉 **You are here:** Part 8 - Fail Gracefully  
**Next:** [Part 9 - 10,000 Threads and a Dream](https://www.codyssey.tech/10000-threads/) →

---

## 📋 Introduction

Errors are not bugs. They're **communication.** When a user sends an invalid latitude of 999.0, that's not a failure of your system — it's a conversation: "Hey, I sent something wrong. What should I fix?" When an external API is down, that's another conversation: "This isn't your fault, but here's what happened and when to try again."

The problem isn't that errors happen. It's that most APIs communicate them terribly. A generic `{"error": "Internal Server Error"}` tells you nothing. A stack trace dumped into JSON is worse — it exposes internals and still doesn't help. And the classic `{"status": 500}` without any detail is the API equivalent of a shrug emoji.

In this article, we'll explore how the Weather Microservice turns errors into structured, helpful, RFC-compliant responses. We'll see the sealed exception hierarchy, the 14-handler `GlobalExceptionHandler`, and the art of making errors useful. ☕

---

## 🎨 The CRAFT Framework: Five Pillars of Error Handling

Meet **CRAFT** — five principles for graceful error handling:

| Letter | Principle                  | What It Means                                                   |
| ------ | -------------------------- | --------------------------------------------------------------- |
| **C**  | **Categorized Exceptions** | Sealed hierarchy with typed exception categories                |
| **R**  | **RFC 7807**               | All errors follow the ProblemDetail standard                    |
| **A**  | **All-layer Validation**   | Errors caught at controller, service, and domain levels         |
| **F**  | **Fallback Handlers**      | Every possible exception has a handler — no unhandled surprises |
| **T**  | **Type-safe Properties**   | Custom properties (category, violations) on error responses     |

---

## 🏗️ The Sealed Exception Hierarchy

Java's sealed classes (Java 17+) create a closed hierarchy — only specified subclasses can extend the base class:

```java
public abstract sealed class WeatherServiceException extends RuntimeException
    permits LocationNotFoundException, WeatherDataNotFoundException, WeatherApiException {

  protected WeatherServiceException(String message) {
    super(message);
  }

  protected WeatherServiceException(String message, Throwable cause) {
    super(message, cause);
  }

  public abstract String getCategory();
}

```

### Why Sealed?

The `sealed` keyword provides **compile-time exhaustiveness checking**. When you handle a `WeatherServiceException`, the compiler knows there are exactly three possible types:

```java
// The compiler knows these are ALL possibilities
switch (exception) {
    case LocationNotFoundException e -> handleNotFound(e);
    case WeatherDataNotFoundException e -> handleNotFound(e);
    case WeatherApiException e -> handleApiError(e);
    // No default needed — the compiler knows this is exhaustive
}

```

Compare to a regular class hierarchy:

```java
// Without sealed: anyone can add new subclasses
// The switch is never truly exhaustive
// You always need a default case

```

### Three Exception Types, Three Concerns

| Exception                    | HTTP Status | Category      | When                                 |
| ---------------------------- | ----------- | ------------- | ------------------------------------ |
| LocationNotFoundException    | 404         | LOCATION      | Location ID doesn't exist in DB      |
| WeatherDataNotFoundException | 404         | WEATHER\_DATA | No weather records for a location    |
| WeatherApiException          | 503         | EXTERNAL\_API | External API failure, timeout, error |

Each exception has a `getCategory()` method that returns a string identifier. This category appears in API responses, helping clients programmatically distinguish between error types.

---

## 📋 The GlobalExceptionHandler: 14 Handlers for Every Error

The `GlobalExceptionHandler` is a `@RestControllerAdvice` class that catches every possible exception and transforms it into an RFC 7807 `ProblemDetail` response:

### Handler 1-3: Domain Exceptions

```java
@ExceptionHandler(LocationNotFoundException.class)
public ProblemDetail handleLocationNotFound(LocationNotFoundException ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.NOT_FOUND, ex.getMessage());
    problem.setType(URI.create(problemBaseUrl + "/location-not-found"));
    problem.setTitle("Location Not Found");
    problem.setProperty("category", ex.getCategory());
    problem.setProperty("timestamp", Instant.now());
    return problem;
}

@ExceptionHandler(WeatherApiException.class)
public ProblemDetail handleWeatherApiException(WeatherApiException ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.SERVICE_UNAVAILABLE,
        "Failed to fetch weather data from external API: " + ex.getMessage());
    problem.setType(URI.create(problemBaseUrl + "/weather-api-error"));
    problem.setTitle("Weather API Error");
    problem.setProperty("category", ex.getCategory());
    problem.setProperty("timestamp", Instant.now());
    return problem;
}

```

Example response:

```json
{
  "type": "https://weatherspring.com/problems/location-not-found",
  "title": "Location Not Found",
  "status": 404,
  "detail": "Location not found with id: 42",
  "category": "LOCATION",
  "timestamp": "2024-01-15T14:30:00Z"
}

```

### Handler 4: Rate Limit Exceeded (429)

```java
@ExceptionHandler(RequestNotPermitted.class)
public ProblemDetail handleRateLimitExceeded(RequestNotPermitted ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.TOO_MANY_REQUESTS,
        "Rate limit exceeded. Please try again later.");
    problem.setType(URI.create(problemBaseUrl + "/rate-limit-exceeded"));
    problem.setTitle("Rate Limit Exceeded");
    problem.setProperty("timestamp", Instant.now());
    return problem;
}

```

This catches Resilience4j's `RequestNotPermitted` exception from the rate limiter (Part 5) and converts it to a proper 429 response.

### Handler 5: Database Conflicts (409)

```java
@ExceptionHandler(DataIntegrityViolationException.class)
public ProblemDetail handleDataIntegrityViolation(DataIntegrityViolationException ex) {
    String detail = "A database constraint was violated.";
    if (ex.getMessage() != null && ex.getMessage().contains("Unique index")) {
      detail = "This record already exists in the database.";
    }

    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.CONFLICT, detail);
    problem.setType(URI.create(problemBaseUrl + "/data-integrity-violation"));
    problem.setTitle("Data Integrity Violation");
    problem.setProperty("timestamp", Instant.now());
    return problem;
}

```

Notice the smart message extraction — if the constraint violation mentions "Unique index", the user gets a helpful "record already exists" message instead of a raw database error.

### Handlers 6-9: Validation Exceptions

The Weather Microservice handles four distinct validation exception types:

```java
// Handler 6: @Valid on @RequestBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidationException(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new LinkedHashMap<>();
    ex.getBindingResult().getAllErrors().forEach(error -> {
        String fieldName = ((FieldError) error).getField();
        String message = error.getDefaultMessage();
        errors.put(fieldName, message);
    });

    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.BAD_REQUEST, "Validation failed for one or more fields");
    problem.setProperty("errors", errors);
    problem.setProperty("timestamp", Instant.now());
    return problem;
}

// Handler 7: @NotNull/@Min/@Max on method parameters
@ExceptionHandler(ConstraintViolationException.class)
public ProblemDetail handleConstraintViolation(ConstraintViolationException ex) {
    Map<String, String> violations = new LinkedHashMap<>();
    ex.getConstraintViolations().forEach(violation ->
        violations.put(violation.getPropertyPath().toString(), violation.getMessage()));

    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.BAD_REQUEST, "Constraint validation failed");
    problem.setProperty("violations", violations);
    return problem;
}

// Handler 8: Spring 6.1+ method validation
@ExceptionHandler(HandlerMethodValidationException.class)
public ProblemDetail handleHandlerMethodValidation(HandlerMethodValidationException ex) {
    Map<String, String> violations = new LinkedHashMap<>();
    ex.getAllValidationResults().forEach(result -> {
        String parameterName = result.getMethodParameter().getParameterName();
        result.getResolvableErrors().forEach(error ->
            violations.put(parameterName, error.getDefaultMessage()));
    });

    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.BAD_REQUEST, "Method validation failed");
    problem.setProperty("violations", violations);
    return problem;
}

// Handler 9: Generic Jakarta validation
@ExceptionHandler(ValidationException.class)
public ProblemDetail handleValidationException(ValidationException ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.BAD_REQUEST, ex.getMessage());
    return problem;
}

```

Example validation error response:

```json
{
  "type": "https://weatherspring.com/problems/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "Validation failed for one or more fields",
  "errors": {
    "latitude": "Latitude must be between -90 and 90",
    "name": "Name must be between 2 and 100 characters"
  },
  "timestamp": "2024-01-15T14:30:00Z"
}

```

### Handler 10: Type Mismatch

```java
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ProblemDetail handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
    String detail = String.format(
        "Invalid value '%s' for parameter '%s'. Expected type: %s",
        ex.getValue(), ex.getName(),
        ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown");

    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.BAD_REQUEST, detail);
    problem.setProperty("parameter", ex.getName());
    problem.setProperty("value", ex.getValue());
    problem.setProperty("expectedType",
        ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown");
    return problem;
}

```

When someone sends `GET /api/weather/current/location/abc` (where `abc` should be a number), this handler produces:

```json
{
  "title": "Type Mismatch",
  "status": 400,
  "detail": "Invalid value 'abc' for parameter 'locationId'. Expected type: Long",
  "parameter": "locationId",
  "value": "abc",
  "expectedType": "Long"
}

```

### Handler 11: The ServletException Unwrapper

```java
@ExceptionHandler(ServletException.class)
public ProblemDetail handleServletException(ServletException ex) {
    Throwable rootCause = ex.getRootCause();

    if (rootCause instanceof HandlerMethodValidationException) {
        return handleHandlerMethodValidation((HandlerMethodValidationException) rootCause);
    } else if (rootCause instanceof ConstraintViolationException) {
        return handleConstraintViolation((ConstraintViolationException) rootCause);
    } else if (rootCause instanceof ValidationException) {
        return handleValidationException((ValidationException) rootCause);
    }

    // Generic servlet error
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.INTERNAL_SERVER_ERROR,
        "A servlet error occurred. Please try again later.");
    return problem;
}

```

> 🔥 **Critical Insight:** In Spring Boot 3.5+, validation exceptions on controller method parameters get wrapped in `ServletException`. Without this unwrapper, validation errors would return 500 instead of 400\. This handler peels off the wrapper and delegates to the correct specific handler.

### Handler 12-13: Sealed Hierarchy Fallback and Catch-All

```java
// Catches any WeatherServiceException not matched by specific handlers
@ExceptionHandler(WeatherServiceException.class)
public ProblemDetail handleWeatherServiceException(WeatherServiceException ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
    problem.setProperty("category", ex.getCategory());
    return problem;
}

// The ultimate catch-all — nothing escapes
@ExceptionHandler(Exception.class)
public ProblemDetail handleGenericException(Exception ex) {
    log.error("Unexpected error occurred", ex);
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.INTERNAL_SERVER_ERROR,
        "An unexpected error occurred. Please try again later.");
    return problem;
}

```

The catch-all handler is the safety net. No matter what exception occurs, the API returns a structured RFC 7807 response instead of a raw stack trace. The generic message hides internal details from clients while the `log.error` records the full stack trace for debugging.

---

## 📐 RFC 7807: The ProblemDetail Standard

Every error response follows RFC 7807 `ProblemDetail`:

```json
{
  "type": "https://weatherspring.com/problems/location-not-found",
  "title": "Location Not Found",
  "status": 404,
  "detail": "Location not found with id: 42",
  "instance": "/api/locations/42",
  "category": "LOCATION",
  "timestamp": "2024-01-15T14:30:00Z"
}

```

| Field         | RFC 7807  | Purpose                                            |
| ------------- | --------- | -------------------------------------------------- |
| type          | Required  | URI identifying the error type (for documentation) |
| title         | Required  | Human-readable summary                             |
| status        | Required  | HTTP status code                                   |
| detail        | Optional  | Specific explanation of this occurrence            |
| instance      | Optional  | URI identifying this specific occurrence           |
| Custom fields | Extension | category, timestamp, errors, violations            |

The `type` URI points to documentation about this error. Clients can use it for programmatic error handling:

```javascript
if (error.type.endsWith('/rate-limit-exceeded')) {
    // Wait and retry
} else if (error.type.endsWith('/location-not-found')) {
    // Show "Location not found" message
}

```

### Enabling RFC 7807 in Spring Boot

```yaml
spring:
  mvc:
    problemdetails:
      enabled: true

```

This single setting tells Spring Boot to use `ProblemDetail` for its built-in error responses (404 Not Found, 405 Method Not Allowed, etc.). Combined with the `GlobalExceptionHandler`, every error in the application — whether thrown by your code, Spring MVC, or the servlet container — returns a consistent RFC 7807 response.

---

## 🔗 The Error Flow: From Exception to Response

```
1. Exception Thrown
   LocationNotFoundException("Location not found with id: 42")

2. Spring MVC catches exception, routes to @ExceptionHandler

3. GlobalExceptionHandler.handleLocationNotFound()
   +- log.warn("Location not found: {}", ex.getMessage())
   +- Create ProblemDetail(404, message)
   +- Set type URI, title, category, timestamp
   +- Return ProblemDetail

4. Spring MVC serializes to JSON
   +- Content-Type: application/problem+json

5. HTTP Response
   HTTP/1.1 404 Not Found
   Content-Type: application/problem+json
   {
     "type": ".../location-not-found",
     "title": "Location Not Found",
     "status": 404,
     "detail": "Location not found with id: 42",
     "category": "LOCATION",
     "timestamp": "2024-01-15T14:30:00Z"
   }

```

Notice the `Content-Type: application/problem+json` — this is the RFC 7807 media type that tells clients this is a structured error response, not a regular JSON body.

---

## ✅ Error Handling Checklist

- \[ \] **Sealed exception hierarchy** — Compile-time exhaustiveness for domain exceptions
- \[ \] **RFC 7807 ProblemDetail** for all error responses — `problemdetails.enabled: true`
- \[ \] **Specific handlers first** — LocationNotFoundException before generic Exception
- \[ \] **Validation errors include field details** — `errors` map with field names and messages
- \[ \] **Type mismatch includes expected type** — Tells clients exactly what format to use
- \[ \] **Rate limit returns 429** — Not 500, with a helpful retry message
- \[ \] **Database conflicts return 409** — With smart message extraction
- \[ \] **ServletException unwrapper** — Handles Spring Boot 3.5+ validation wrapping
- \[ \] **Catch-all handler** — No exception ever returns a raw stack trace
- \[ \] **Generic error hides internals** — "An unexpected error occurred" in the response, full stack trace in logs
- \[ \] **Custom properties** — `category`, `timestamp`, `violations` for programmatic error handling

---

## 🎓 Conclusion: Errors Are Communication

Errors are the most underrated part of an API. They're the first thing a developer sees when something goes wrong, and the quality of those error messages determines whether they fix the problem in 5 minutes or 5 hours:

1. **The CRAFT framework** (Categorized exceptions, RFC 7807, All-layer validation, Fallback handlers, Type-safe properties) guides error handling design
2. **Sealed exception hierarchies** (Java 17+) provide compile-time guarantees that all exception types are handled
3. **RFC 7807 ProblemDetail** standardizes error responses with `type`, `title`, `status`, and `detail` fields
4. **14 exception handlers** cover every error scenario — from validation failures to rate limits to database conflicts
5. **Validation errors** include field-level details so clients know exactly what to fix
6. **The ServletException unwrapper** handles Spring Boot 3.5+'s validation exception wrapping
7. **Custom properties** (`category`, `violations`, `expectedType`) enable programmatic error handling by clients
8. **The catch-all handler** ensures no exception ever results in a raw stack trace

Errors are not failures — they're conversations between your API and its consumers. Make those conversations clear, structured, and helpful.

**Coming Next Week:**  
Part 9: 10,000 Threads and a Dream - Virtual Threads and Concurrent Microservices 🚀

---

📚 **Series Progress**

✅ Part 1: The Blueprint Before the Build  
✅ Part 2: Spring Boot Alchemy  
✅ Part 3: REST Assured  
✅ Part 4: The Data Foundation  
✅ Part 5: When the World Breaks  
✅ Part 6: Cache Me If You Can  
✅ Part 7: Guarding the Gates  
✅ Part 8: Fail Gracefully ← You just finished this!  
⬜ Part 9: 10,000 Threads and a Dream  
⬜ Part 10: Can You See Me Now?  
⬜ Part 11: Trust, But Verify  
⬜ Part 12: Ship It  
⬜ Part 13: To Production and Beyond

---

*Happy coding, and remember — the best error message is the one that tells you exactly what went wrong and how to fix it.* ☕