skip to content

Client Error Handling

By default a 4xx or 5xx becomes an exception, and ResponseErrorHandler or onStatus lets you decide what a failure means for your code. Interviewers probe here because swallowing a client error is how corrupt data spreads.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What happens by default when a Spring REST client (RestTemplate or RestClient) receives a 4xx or 5xx HTTP response?

level: juniorimportance: must knowfreq 78%

answer

  1. DefaultResponseErrorHandler: 4xx/5xx = error
  2. 4xx -> HttpClientErrorException, 5xx -> HttpServerErrorException
  3. both extend RestClientResponseException
  4. ResourceAccessException = I/O, no response
  5. all unchecked RuntimeExceptions

basics

~10 s

By default the client treats 4xx/5xx as errors and throws an exception: HttpClientErrorException for 4xx and HttpServerErrorException for 5xx. A successful 2xx just returns the body normally.

solid answer

~30 s

Spring's REST clients ship with a DefaultResponseErrorHandler that classifies any 4xx or 5xx status as an error. On RestTemplate calls and on RestClient's retrieve() path, that handler throws automatically: HttpClientErrorException for 4xx (400/404/etc.), HttpServerErrorException for 5xx (500/503/etc.), and UnknownHttpStatusCodeException for non-standard codes. All three extend RestClientResponseException, which exposes the status code, response headers, and the raw error body. A network/IO failure (connection refused, read timeout) is different — that surfaces as ResourceAccessException, not a status exception, because there was no HTTP response at all. So by default you don't check status codes manually; you catch exceptions.

code

java · 17 lines
java
RestClient client = RestClient.create();

try {
    // .retrieve() applies default status handling: 4xx/5xx throw here
    User user = client.get()
            .uri("https://api.example.com/users/{id}", 42)
            .retrieve()
            .body(User.class);
} catch (HttpClientErrorException e) {      // 4xx
    // e.g. 404 -> user not found
    HttpStatusCode status = e.getStatusCode();
    String errorBody = e.getResponseBodyAsString();
} catch (HttpServerErrorException e) {      // 5xx
    // downstream is broken; maybe retry or fail fast
} catch (ResourceAccessException e) {       // no HTTP response at all (timeout/connection refused)
    // network problem
}

go deeper

for a junior

Must know that 4xx/5xx throw by default and the two exception names.

for a middle

Should name RestClientResponseException as the common parent and know where the body lives.

for a senior

Distinguishes retrieve() default handling from exchange() (no default handling) and ResourceAccessException from status exceptions.

for a principal

Frames default behavior as a policy that must be intentionally shaped for resilience across many downstream calls.

## The default contract Spring's synchronous HTTP clients — the older `RestTemplate` and the newer fluent `RestClient` — both wire in a `DefaultResponseErrorHandler` out of the box. This class implements the `ResponseErrorHandler` interface, which decides two things: (1) *is this response an error?* via `hasError(...)`, and (2) *what do we do about it?* via `handleError(...)`. The default `hasError` returns `true` for any status in the **4xx (client error)** or **5xx (server error)** series. Everything else (1xx, 2xx, 3xx) is treated as non-error. ## What gets thrown When a response is an error, the default handler throws a subtype of `RestClientResponseException`: - **`HttpClientErrorException`** for **4xx** statuses (e.g. 400 Bad Request, 401 Unauthorized, 404 Not Found). - **`HttpServerErrorException`** for **5xx** statuses (e.g. 500 Internal Server Error, 503 Service Unavailable). - **`UnknownHttpStatusCodeException`** when the numeric status doesn't map to a known series/value. All of these carry the response so you can inspect it: `getStatusCode()`, `getStatusText()`, `getResponseHeaders()`, `getResponseBodyAsString()` / `getResponseBodyAsByteArray()`, and `getResponseBodyAs(Class)` to deserialize the error payload. ## The full exception hierarchy ``` RestClientException (root, unchecked) ├─ ResourceAccessException (I/O: no HTTP response) └─ RestClientResponseException (got a response w/ a status) ├─ UnknownHttpStatusCodeException └─ HttpStatusCodeException ├─ HttpClientErrorException (4xx) → NotFound, BadRequest, ... └─ HttpServerErrorException (5xx) → InternalServerError, ... ``` Key distinction for juniors: a **status exception** means the server answered but with an error status; a **`ResourceAccessException`** means the request never completed at the HTTP level (DNS failure, connection refused, socket/read timeout). All are unchecked (`RuntimeException`), so the compiler won't force you to handle them. ## Where the difference matters between the two clients - **RestTemplate**: every method (`getForObject`, `exchange`, ...) runs the error handler, so 4xx/5xx always throw unless you replace the handler. - **RestClient `.retrieve()`**: applies the default status handling, so `body(...)`/`toEntity(...)` throw on 4xx/5xx. - **RestClient `.exchange(...)`**: the escape hatch — it does **not** apply the default status handlers; you get the raw `ClientHttpResponse` and decide everything yourself. ## Gotchas - Redirects (3xx) are *not* errors here; the underlying request factory usually follows them. - The error body is only readable once and is buffered by the exception — read it from the exception, not by re-reading the stream. - Because these are unchecked, an unhandled 500 from a downstream call will bubble up and (in a controller) typically become your own 500 unless you translate it.

  • What exception do you get for a connection timeout versus a 500 response?
    A connection/read timeout (no HTTP response) surfaces as ResourceAccessException; a 500 response surfaces as HttpServerErrorException (a RestClientResponseException carrying the status and body).
  • Are these exceptions checked or unchecked?
    Unchecked — they all extend RestClientException which extends RuntimeException, so the compiler doesn't force a try/catch.

saying these in an interview costs you the question

  • Claiming the client returns null or an empty body on 4xx/5xx instead of throwing.
  • Thinking you must manually check response.getStatusCode() with RestTemplate — the default handler already throws.
  • Confusing a timeout (ResourceAccessException) with a 5xx status exception.

context

open as a page

How do you customize error handling for specific status codes with RestClient's onStatus handlers?

level: middleimportance: must knowfreq 70%

basics

~20 s

On the retrieve() path, chain .onStatus(predicate, handler). The predicate tests the HttpStatusCode; the handler receives the request and ClientHttpResponse so you can throw your own exception or read the body. Matching handlers replace the default throw.

open as a page

Given an HttpClientErrorException or HttpServerErrorException, how do you extract the status, headers, and deserialize the error response body?

level: seniorimportance: should knowfreq 50%

basics

~10 s

These extend RestClientResponseException, which gives getStatusCode(), getResponseHeaders(), getResponseBodyAsString(), and getResponseBodyAs(Class) to deserialize the error payload into a POJO. getStatusText() gives the reason phrase.

open as a page

What is the ResponseErrorHandler interface and how do you plug a custom one into RestTemplate or RestClient?

level: seniorimportance: should knowfreq 55%

basics

~10 s

ResponseErrorHandler is the strategy Spring uses to decide if a response is an error and handle it. It has hasError(response) and handleError(response). Set a custom one via RestTemplate.setErrorHandler(...) or RestClient.Builder.defaultStatusHandler(...).

open as a page

How would you design a robust, cross-cutting error-handling strategy for many downstream RestClient calls, and how does exchange() fit in?

level: principalimportance: should knowfreq 38%

basics

~20 s

Centralize policy on the client builder (defaultStatusHandler) to translate 4xx/5xx into a small set of domain exceptions with the body attached; distinguish those from ResourceAccessException (I/O). Use exchange() where you need full manual status control, and combine with retries/circuit breakers.

open as a page