eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – Summer Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

Course – Summer Sale 2026 – NPI (cat=Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

1. Overview

We expect HTTP API calls between microservices to encounter occasional errors.

In Spring Boot with OpenFeign, the default error handler propagates downstream errors, such as Not Found, as Internal Server Error. This is seldom the best way to convey the error. However, both Spring and OpenFeign allow us to provide our own error handling.

In this article, we’ll see how default exception propagation works. We’ll also learn how to supply our own errors.

2. Default Exception Propagation Strategy

The Feign client makes interactions between microservices straightforward and highly configurable, using annotations and configuration properties. However, API calls might fail due to any random technical reason, bad user requests, or coding errors.

Fortunately, Feign and Spring have a sensible default implementation for error handling.

2.1. Default Exception Propagation in Feign

Feign uses the ErrorDecoder.Default class for its error handling. With this, whenever Feign receives any non-2xx status code, it passes that to the ErrorDecoder’s decode method. The decode method either returns a RetryableException if the HTTP response had a Retry-After header or it returns a FeignException otherwise. When retrying, if the request fails after the default number of retries, then the FeignException will be returned.

The decode method stores the HTTP method key and response in the FeignException.

2.2. Default Exception Propagation in Spring Rest Controller

Whenever the RestController receives any unhandled exception, it returns a 500 Internal Server Error response to the client.

Also, Spring provides a well-structured error response with details such as timestamp, HTTP status code, error, and the path:

{
    "timestamp": "2022-07-08T08:07:51.120+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/myapp1/product/Test123"
}

Let’s take a deep dive into this with an example.

3. Example Application

Let’s imagine we need to build a simple microservice that returns product information from another external service.

First, let’s model the Product class with a few properties:

public class Product {
    private String id;
    private String productName;
    private double price;
}

Then, let’s implement the ProductController with the Get Product endpoint:

@RestController("product_controller")
@RequestMapping(value ="myapp1")
public class ProductController {

    private ProductClient productClient;

    @Autowired
    public ProductController(ProductClient productClient) {
        this.productClient = productClient;
    }

    @GetMapping("/product/{id}")
    public Product getProduct(@PathVariable String id) {
        return productClient.getProduct(id);
    }
}

Next, let’s see how to register the Feign Logger as a Bean:

public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

Finally, let’s implement the ProductClient to interface with the external API:

@FeignClient(name = "product-client", url="http://localhost:8081/product/", configuration = FeignConfig.class)
public interface ProductClient {
    @RequestMapping(value = "{id}", method = RequestMethod.GET")
    Product getProduct(@PathVariable(value = "id") String id);
}

Let’s now explore default error propagation using the above example.

4. Default Exception Propagation

4.1. Using WireMock Server

To experiment, we’ll need to use a mocking framework to simulate the service we’re calling.

First, let’s include the WireMockServer Maven dependency:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <version>2.33.2</version>
    <scope>test</scope>
</dependency>

Then, let’s configure and start the WireMockServer:

WireMockServer wireMockServer = new WireMockServer(8081);
configureFor("localhost", 8081);
wireMockServer.start();

The WireMockServer is started at the same host and port that the Feign client is configured to use.

4.2. Default Exception Propagation in Feign Client

Feign’s default error handler, ErrorDecoder.Default, always throws a FeignException.

Let’s mock the getProduct method with the WireMock.stubFor to make it appear to be unavailable:

String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));

assertThrows(FeignException.class, () -> productClient.getProduct(productId));

In the above test case, the ProductClient throws the FeignException when it encounters the 503 error from the downstream service.

Next, let’s try the same experiment but with a 404 Not Found response:

String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.NOT_FOUND.value())));

assertThrows(FeignException.class, () -> productClient.getProduct(productId));

Again, we’re getting a general FeignException. In this situation, perhaps the user requested something that was wrong and our Spring application needs to know that it’s a bad user request so that it can handle things differently.

We should note that FeignException does have a status property containing the HTTP status code, but a try/catch strategy routes exceptions based on their type, rather than their properties.

4.3. Default Exception Propagation in Spring Rest Controller

Let’s now see how the FeignException propagates back to the requester.

When the ProductController gets the FeignException from the ProductClientit passes that to its default error handling implementation provided by the framework.

Let’s assert when the product service is unavailable:

String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));

mockMvc.perform(get("/myapp1/product/" + productId))
  .andExpect(status().is(HttpStatus.INTERNAL_SERVER_ERROR.value()));

Here, we can see that we get the Spring INTERNAL_SERVER_ERROR. This default behavior is not always the best, as different service errors may require different outcomes.

5. Propagating Custom Exceptions in Feign With the ErrorDecoder

Instead of always returning the default FeignException, we should return some application-specific exceptions based on the HTTP status code.

Let’s override the decode method in a custom ErrorDecoder implementation:

public class CustomErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        switch (response.status()){
            case 400:
                return new BadRequestException();
            case 404:
                return new ProductNotFoundException("Product not found");
            case 503:
                return new ProductServiceNotAvailableException("Product Api is unavailable");
            default:
                return new Exception("Exception while getting product details");
        }
    }
}

In our custom decode method, we’re returning different exceptions with a few application-specific ones to provide more context for the actual problem. We can also include more details in the application-specific exception messages.

We should note that the decode method returns the FeignException rather than throwing it.

Now, let’s configure the CustomErrorDecoder in the FeignConfig as a Spring Bean:

@Bean
public ErrorDecoder errorDecoder() {
   return new CustomErrorDecoder();
}

Alternatively, the CustomErrorDecoder can be configured directly in the ProductClient:

@FeignClient(name = "product-client-2", url = "http://localhost:8081/product/", 
   configuration = { FeignConfig.class, CustomErrorDecoder.class })

Then, let’s check whether the CustomErrorDecoder returns ProductServiceNotAvailableException:

String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));

assertThrows(ProductServiceNotAvailableException.class, 
  () -> productClient.getProduct(productId));

Again, let’s write a test case to assert the ProductNotFoundException when the product is not present:

String productId = "test";
stubFor(get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.NOT_FOUND.value())));

assertThrows(ProductNotFoundException.class, 
  () -> productClient.getProduct(productId));

While we’re now providing a variety of exceptions from the Feign client, Spring will still produce a generic internal server error when it catches them all. Since this is not what we desire, let’s see how we can improve on that.

6. Propagating Custom Exceptions in Spring Rest Controller

As we’ve seen, the default Spring Boot error handler provides a generic error response. API Consumers might need detailed information with relevant error responses. Ideally, the error response should be able to explain the problem and help in debugging.

We could override the default exception handler in the RestController in many ways.

We’ll look into one such approach to handle errors with the RestControllerAdvice annotation.

6.1. Using @RestControllerAdvice

The @RestControllerAdvice annotation allows us to consolidate multiple exceptions into a single, global error handling component. 

Let’s imagine a scenario where the ProductController needs to return a different custom error response based on the downstream error.

First, let’s create the ErrorResponse class to customize the error response:

public class ErrorResponse {

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    private Date timestamp;

    @JsonProperty(value = "code")
    private int code;

    @JsonProperty(value = "status")
    private String status;
    
    @JsonProperty(value = "message")
    private String message;
    
    @JsonProperty(value = "details")
    private String details;
}

Now, let’s subclass the ResponseEntityExceptionHandler and include the @ExceptionHandler annotation with the error handlers:

@RestControllerAdvice
public class ProductExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ProductServiceNotAvailableException.class})
    public ResponseEntity<ErrorResponse> handleProductServiceNotAvailableException(ProductServiceNotAvailableException exception, WebRequest request) {
        return new ResponseEntity<>(new ErrorResponse(
          HttpStatus.INTERNAL_SERVER_ERROR,
          exception.getMessage(),
          request.getDescription(false)),
          HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler({ProductNotFoundException.class})
    public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException exception, WebRequest request) {
        return new ResponseEntity<>(new ErrorResponse(
          HttpStatus.NOT_FOUND,
          exception.getMessage(),
          request.getDescription(false)),
          HttpStatus.NOT_FOUND);
    }
}

In the above code, the ProductServiceNotAvailableException returns as an INTERNAL_SERVER_ERROR response to the client. In contrast, a user-specific error like ProductNotFoundException is handled differently and returns as a NOT_FOUND response.

6.2. Testing the Spring Rest Controller

Let’s test the ProductController when product service is unavailable:

String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.SERVICE_UNAVAILABLE.value())));

MvcResult result = mockMvc.perform(get("/myapp2/product/" + productId))
  .andExpect(status().isInternalServerError()).andReturn();

ErrorResponse errorResponse = objectMapper.readValue(result.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(500, errorResponse.getCode());
assertEquals("Product Api is unavailable", errorResponse.getMessage());

Again, let’s test the same ProductController but with a product not found error:

String productId = "test";
stubFor(WireMock.get(urlEqualTo("/product/" + productId))
  .willReturn(aResponse()
  .withStatus(HttpStatus.NOT_FOUND.value())));

MvcResult result = mockMvc.perform(get("/myapp2/product/" + productId))
  .andExpect(status().isNotFound()).andReturn();

ErrorResponse errorResponse = objectMapper.readValue(result.getResponse().getContentAsString(), ErrorResponse.class);
assertEquals(404, errorResponse.getCode());
assertEquals("Product not found", errorResponse.getMessage());

The above tests show how the ProductController returns different error responses based on the downstream error.

If we hadn’t implemented our CustomErrorDecoder, then the RestControllerAdvice is required to handle the default FeignException as a fallback to have a generic error response.

7. Conclusion

In this article, we’ve explored how the default error handling is implemented in Feign and Spring.

Also, we’ve seen how we can customize that in Feign client with CustomErrorDecoder and in the Rest Controller with RestControllerAdvice.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – Summer Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

Course – Summer Sale 2026 – NPI (All)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

eBook Jackson – NPI EA – 3 (cat = Jackson)
eBook – eBook Guide Spring Cloud – NPI (cat=Cloud/Spring Cloud)