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 – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

1. Overview

In this tutorial, we’ll examine the role of interceptors in gRPC server applications to handle global exceptions.

Interceptors can validate or manipulate the request before it reaches the RPC methods. Hence, they are useful in handling common concerns like logging, security, caching, auditing, authentication and authorization, and much more for applications.

Applications may also use interceptors as global exception handlers.

2. Interceptors as Global Exception Handlers

Majorly, interceptors can help handle exceptions of two types:

  • Handle unknown runtime exceptions escaping from methods that couldn’t handle them
  • Handle exceptions that escape from any other downstream interceptors

Interceptors can help create a framework to handle exceptions in a centralized manner. This way the applications can have a consistent standard and robust approach to handle exceptions.

They can treat exceptions in various ways:

  • Log or persist the exceptions for auditing or reporting purposes
  • Create support tickets
  • Modify or enrich the error responses before sending it back to the clients

3. High-Level Design of a Global Exception Handler

The interceptor can forward the incoming request to the target RPC service. However, when the target RPC method throws an exception back, it can capture it and then handle it appropriately.

Let’s assume there’s an order processing microservice. We’ll develop a global exception handler with the help of an interceptor to catch the exception escaped from the RPC methods in the microservice. Additionally, the interceptor catches the exceptions that escaped from any of the downstream interceptors. Then, it calls a ticket service to raise tickets in a ticketing system. Finally, the response is sent back to the client.

Let’s take a look at the traversal path of the request when it fails in the RPC endpoint:

 

interceptor sequence

Similarly, let’s see the traversal path of the request when it fails in the log interceptor:

 

interceptor sequence failed at log

First, we’ll begin defining the base classes for the order processing service in the protobuf file order_processing.proto:

syntax = "proto3";

package orderprocessing;

option java_multiple_files = true;
option java_package = "com.baeldung.grpc.orderprocessing";

message OrderRequest {
  string product = 1;
  int32 quantity = 2;
  float price = 3;
}
message OrderResponse {
  string response = 1;
  string orderID = 2;
  string error = 3;
}
service OrderProcessor {
  rpc createOrder(OrderRequest) returns (OrderResponse){}
}

The order_processing.proto file defines OrderProcessor with a remote method createOrder() and two DTOs OrderRequest and OrderResponse.

Let’s have a look at the major classes we’ll implement in the upcoming sections:

 

order processing cld

Later, we can use the order_processing.proto file for generating the supporting Java source code for implementing OrderProcessorImpl and GlobalExeptionInterceptor. The Maven plugin generates the classes OrderRequest, OrderResponse, and OrderProcessorGrpc.

We’ll discuss each of these classes in the implementation section.

4. Implementation

We’ll implement an interceptor that can handle all kinds of exceptions. The exception could be explicitly raised due to some failed logic or it could be an exception due to some unforeseen error.

4.1. Implement Global Exception Handler

Interceptors in a gRPC application have to implement the interceptCall() method of the ServerInterceptor interface:

public class GlobalExceptionInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata headers,
        ServerCallHandler<ReqT, RespT> next) {
        ServerCall.Listener<ReqT> delegate = null;
        try {
            delegate = next.startCall(serverCall, headers);
        } catch(Exception ex) {
            return handleInterceptorException(ex, serverCall);
        }
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(delegate) {
            @Override
            public void onHalfClose() {
                try {
                    super.onHalfClose();
                } catch (Exception ex) {
                    handleEndpointException(ex, serverCall);
                }
            }
        };
    }

    private static <ReqT, RespT> void handleEndpointException(Exception ex, ServerCall<ReqT, RespT> serverCall) {
        String ticket = new TicketService().createTicket(ex.getMessage());
        serverCall.close(Status.INTERNAL
            .withCause(ex)
            .withDescription(ex.getMessage() + ", Ticket raised:" + ticket), new Metadata());
    }

    private <ReqT, RespT> ServerCall.Listener<ReqT> handleInterceptorException(Throwable t, ServerCall<ReqT, RespT> serverCall) {
        String ticket = new TicketService().createTicket(t.getMessage());
        serverCall.close(Status.INTERNAL
            .withCause(t)
            .withDescription("An exception occurred in a **subsequent** interceptor:" + ", Ticket raised:" + ticket), new Metadata());

        return new ServerCall.Listener<ReqT>() {
            // no-op
        };
    }
}

The method interceptCall() takes in three input parameters:

  • ServerCall: Helps receive response messages
  • Metadata: Holds the metadata of the incoming request
  • ServerCallHandler: Helps dispatch the incoming server call to the next processor in the interceptor chain

The method has two trycatch blocks. The first one handles the uncaught exception thrown from any subsequent downstream interceptors. In the catch block, we call the method handleInterceptorException() which creates a ticket for the exception. Finally, it returns an object of ServerCall.Listener which is a call-back method.

Similarly, the second trycatch block handles the uncaught exceptions thrown from the RPC endpoints. The interceptCall() method returns ServerCall.Listener that acts as a callback for incoming RPC messages. Specifically, it returns an instance of ForwardingServerCallListener.SimpleForwardingServerCallListener which is a subclass of ServerCall.Listener.

To handle the exception thrown from the downstream methods we’ve overridden the method onHalfClose() in the class ForwardingServerCallListener.SimpleForwardingServerCallListener. It gets invoked once the client has completed sending messages.

In this method, super.onHalfClose() forwards the request to the RPC endpoint createOrder() in the OrderProcessorImpl class. If there’s an uncaught exception in the endpoint, we catch the exception and then call handleEndpointException() to create a ticket. Finally, we call the method close() on the serverCall object to close the server call and send the response back to the client.

4.2. Register Global Exception Handler

We register the interceptor while creating the io.grpc.Server object during the boot-up:

public class OrderProcessingServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        Server server = ServerBuilder.forPort(8080)
          .addService(new OrderProcessorImpl())
          .intercept(new LogInterceptor())
          .intercept(new GlobalExceptionInterceptor())
          .build();
        server.start();
        server.awaitTermination();
    }
}

We pass the GlobalExceptionInterceptor object to the intercept() method of io.grpc.ServerBuilder class. This ensures that any RPC call to the OrderProcessorImpl service goes through GlobalExceptionInterceptor. Similarly, we call the addService() method to register the OrderProcessorImpl service.  At the end, we invoke the start() method on the Server object to start the server application.

4.3. Handle Uncaught Exception From Endpoints

To demonstrate the exception handler, let’s first take a look at the OrderProcessorImpl class:

public class OrderProcessorImpl extends OrderProcessorGrpc.OrderProcessorImplBase {
    @Override
    public void createOrder(OrderRequest request, StreamObserver<OrderResponse> responseObserver) {
        if (!validateOrder(request)) {
             throw new StatusRuntimeException(Status.FAILED_PRECONDITION.withDescription("Order Validation failed"));
        } else {
            OrderResponse orderResponse = processOrder(request);

            responseObserver.onNext(orderResponse);
            responseObserver.onCompleted();
        }
    }

    private Boolean validateOrder(OrderRequest request) {
        int tax = 100/0;
        return false;
    }

    private OrderResponse processOrder(OrderRequest request) {
        return OrderResponse.newBuilder()
          .setOrderID("ORD-5566")
          .setResponse("Order placed successfully")
          .build();
    }
}

The RPC method createOrder() validates the order first and then processes it by calling the processOrder() method. In the validateOrder() method, we deliberately force a runtime exception by dividing a number by zero.

Now, let’s run the service and see how it handles the exception:

@Test
void whenRuntimeExceptionInRPCEndpoint_thenHandleException() {
    OrderRequest orderRequest = OrderRequest.newBuilder()
      .setProduct("PRD-7788")
      .setQuantity(1)
      .setPrice(5000)
      .build();

    try {
        OrderResponse response = orderProcessorBlockingStub.createOrder(orderRequest);
    } catch (StatusRuntimeException ex) {
        assertTrue(ex.getStatus()
          .getDescription()
          .contains("Ticket raised:TKT"));
    }
}

We create the OrderRequest object and then pass it to the createOrder() method in the client stub. As expected, the service throws back the exception. When we inspect the description in the exception, we find the ticket information embedded in it. Hence, it shows that the GlobalExceptionInterceptor did its job.

This is equally effective for streaming cases as well.

4.4. Handle Uncaught Exceptions From Interceptors

Let’s suppose there’s a second interceptor that gets invoked after the GlobalExceptionInterceptor. LogInterceptor logs all the incoming requests for auditing purposes. Let’s take a look at it:

public class LogInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata,
        ServerCallHandler<ReqT, RespT> next) {
        logMessage(serverCall);
        ServerCall.Listener<ReqT> delegate = next.startCall(serverCall, metadata);
        return delegate;
    }

    private <ReqT, RespT> void logMessage(ServerCall<ReqT, RespT> call) {
        int result = 100/0;
    }
}

In LogInterceptor, the interceptCall() method invokes logMessage() to log the messages before forwarding the request to the RPC endpoint. The logMessage() method deliberately performs division by zero to raise a runtime exception for demonstrating the capability of GlobalExceptionInterceptor.

Let’s run the service and see how it handles the exception raised from LogInterceptor:

@Test
void whenRuntimeExceptionInLogInterceptor_thenHandleException() {
    OrderRequest orderRequest = OrderRequest.newBuilder()
        .setProduct("PRD-7788")
        .setQuantity(1)
        .setPrice(5000)
        .build();

    try {
        OrderResponse response = orderProcessorBlockingStub.createOrder(orderRequest);
    } catch (StatusRuntimeException ex) {
        assertTrue(ex.getStatus()
            .getDescription()
            .contains("An exception occurred in a **subsequent** interceptor:, Ticket raised:TKT"));
    }
    logger.info("order processing over");
}

First, we call the createOrder() method on the client stub. This time, the GlobalExceptionInterceptor catches the exception that escaped from the LogInterceptor in the first trycatch block.  Subsequently, the client receives the exception with ticket information embedded in the description.

5. Conclusion

In this article, we explored the role of interceptors in the gRPC framework as global exception handlers. They’re excellent tools for handling common concerns on exceptions, such as logging, creating tickets, enriching error responses, and much more.

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.

eBook Jackson – NPI EA – 3 (cat = Jackson)