Custom Exception Handling for Feign Calls in Spring Distributed Services

This article explains how to implement unified, user‑friendly exception handling for Feign‑based service calls in a Spring distributed architecture by customizing ErrorDecoder, defining result objects, and using global @ControllerAdvice to return clear error codes and messages to front‑end users.

Java Captain
Java Captain
Java Captain
Custom Exception Handling for Feign Calls in Spring Distributed Services

1 Preface

In distributed service scenarios, business services are split into independent modules that call each other. Proper exception handling is critical so that end users see a clear reason for a failure instead of raw Java exceptions such as NullPointerException, IllegalArgumentException, or FeignException, which are confusing for non‑technical users.

2 Service Call Exception Scenarios

This picture shows a typical service‑chain exception: a front‑end request reaches service A, which calls service B; service B throws an exception, and service A returns a fallback error that is not user‑friendly.

The second diagram illustrates a Feign‑generated exception that can be intercepted by a custom FeignException handling class.

3 Rewrite Feign Exception Handling

We can implement Feign's ErrorDecoder interface and override its decode method to provide custom exception handling. For each Feign interface, we throw a custom exception that carries an error code and a user‑friendly message.

FeignExceptionConfiguration – custom exception handler

@Slf4j
@Configuration
public class FeignExceptionConfiguration {
    @Bean
    public ErrorDecoder errorDecoder() {
        return new UserErrorDecoder();
    }
    /**
     * Re‑implement Feign's exception handling to capture JSON‑formatted error information returned by a RESTful API.
     */
    public class UserErrorDecoder implements ErrorDecoder {
        @Override
        public Exception decode(String methodKey, Response response) {
            Exception exception = new MyException();
            ObjectMapper mapper = new ObjectMapper();
            // ignore empty properties
            mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_EMPTY);
            // ignore unknown JSON fields
            mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            // forbid using int for enum order
            mapper.configure(DeserializationConfig.Feature.FAIL_ON_NUMBERS_FOR_ENUMS, true);
            try {
                String json = Util.toString(response.body().asReader());
                log.info("Exception response: " + JSON.toJSONString(json));
                exception = new RuntimeException(json);
                if (StringUtils.isEmpty(json)) {
                    return null;
                }
                FeignFaildResult result = mapper.readValue(json, FeignFaildResult.class);
                // business exception wrapped into custom MyException
                if (result.getCode() != 200) {
                    exception = new MyException(result.getMsg(), result.getCode());
                }
            } catch (IOException ex) {
                log.error(ex.getMessage(), ex);
            }
            return exception;
        }
    }
}

After processing, the exception response only contains code, msg, and data fields, which are directly mapped to a result object and returned via a custom exception.

FeignFaildResult – exception result object

@Data
public class FeignFaildResult {
    private String msg;
    private int code;
}

MyException – custom exception

import lombok.Data;

@Data
public class MyException extends RuntimeException {
    // custom error code
    private int status = 503;

    public MyException() {}

    // constructor with message and status
    public MyException(String message, int status) {
        super(message);
        this.status = status;
    }
}

FeignClient interface definition

@FeignClient(contextId = "iTestServiceClient",
            value = "Lxlxxx-system2",
            fallback = TestServiceFallbackFactory.class,
            configuration = FeignExceptionConfiguration.class)
public interface ITestServiceClient {
    /**
     * Service call test method
     */
    @GetMapping("/test/method")
    public R<String> testRequestMethod() throws Exception;
}

By specifying the configuration attribute of @FeignClient, the custom exception handling is enabled.

Called service

The callee simply throws an exception when a business error occurs.

Result handling

The exception message thrown by the callee is returned directly to the front‑end, making the error information clear.

4 Spring Global Exception Handling

Alternatively, a global exception handler can be used. By annotating a class with @ControllerAdvice and methods with @ExceptionHandler, Spring 3.2+ can intercept exceptions across all controllers.

ResultCode – error code enumeration

First, define a unified error‑code enum.

public enum ResultCode {
    /*
     * Common error code conventions
     * 0   – SUCCESS
     * 10000‑19999 – WARN_
     * 20000‑29999 – ERR_
     * 30000‑39999 – DIY_
     * 40000‑49999 – SYS_
     */
    SUCCESS("0", "Operation successful"),
    ERR_LACK_PARAM("20001", "Invalid request parameters"),
    ERR_NO_LOGIN("20002", "User not logged in"),
    ERR_NO_RIGHT("20003", "No permission to access resource"),
    ERR_NO_SERVICE("20004", "Resource not found"),
    ERR_WRONG_STATUS("20005", "Operation not allowed in current state"),
    ERR_LACK_CONFIG("20006", "Missing configuration"),
    ERR_PROCESS_FAIL("20007", "Business processing failed"),
    ERR_THIRD_API_FAIL("20008", "Third‑party API call failed"),
    ERR_IS_DELETED("20009", "Resource has been deleted"),
    ERR_UPDATE_FAIL("20010", "Update operation failed"),
    SYS_MAINTENANCE("40001", "System under maintenance"),
    SYS_BUSY("40002", "System busy"),
    SYS_EXCEPTION("40003", "System exception");

    private String code;
    private String msg;
    // constructors, getters, setters, utility methods omitted for brevity
}

BaseResult – unified response object

@Data
public class BaseResult<T> implements Serializable {
    private static final long serialVersionUID = 621986096326899992L;
    private String message;
    private String errorCode;
    private T data;
    // static factory methods for success/failure omitted for brevity
    public Boolean isSuccess() {
        return "0".equals(this.errorCode) ? true : false;
    }
}

CommonException – custom global exception class

public class CommonException extends RuntimeException {
    private String code;
    /**
     * Custom constructor with code and message
     */
    public CommonException(String code, String message) {
        super(message);
        this.code = code;
    }
    /**
     * Constructor based on ResultCode enum
     */
    public CommonException(ResultCode resultCode) {
        super(resultCode.getMsg());
        this.code = resultCode.getCode();
    }
}

ExceptionController – global exception handling controller

@ControllerAdvice
public class ExceptionController {
    /**
     * Handle CommonException and return a unified BaseResult
     */
    @ExceptionHandler(CommonException.class)
    @ResponseBody
    public BaseResult handlerException(CommonException e) {
        // return false result with error code and message
        return new BaseResult(e.getMessage(), e.getCode());
    }
}

5 Invocation Result

@RestController
@Slf4j
public class TestController {
    @Autowired
    private ITestServiceClient iTestServiceClient;

    @GetMapping("/testMethod")
    public BaseResult testMethod() throws Exception {
        try {
            log.info("Calling system2 service via Feign …");
            R<String> stringR = iTestServiceClient.testRequestMethod();
        } catch (Exception e) {
            throw new CommonException(ResultCode.SYS_EXCEPTION.getCode(), ResultCode.SYS_EXCEPTION.getMsg());
        }
        return BaseResult.success();
    }
}

In this scenario, service A calls service B, B throws an exception, the ExceptionController captures it, and the custom error code and message are returned to the client, providing a clear error description.

6 Summary

The two approaches described above handle exceptions between services from different perspectives. The concrete strategy should be chosen according to business requirements; exceptions can be categorized (e.g., basic, parameter validation, utility, business‑check) and defined separately to form a standardized exception‑handling framework.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaException Handlingspring
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.