0
# Exception Handling
1
2
Comprehensive exception handling system with customizable error transformation and gRPC status code mapping. The system provides hooks for converting application exceptions into proper gRPC status responses.
3
4
## Capabilities
5
6
### ExceptionHandler Abstract Class
7
8
Generic exception handler that intercepts exceptions during gRPC call processing and provides customization points for error transformation.
9
10
```java { .api }
11
/**
12
* Generic exception handler
13
*/
14
public abstract class ExceptionHandler<ReqT, RespT> extends
15
ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT> {
16
17
private final ServerCall<ReqT, RespT> call;
18
private final Metadata metadata;
19
20
public ExceptionHandler(ServerCall.Listener<ReqT> listener,
21
ServerCall<ReqT, RespT> call,
22
Metadata metadata) {
23
super(listener);
24
this.metadata = metadata;
25
this.call = call;
26
}
27
28
protected abstract void handleException(Throwable t,
29
ServerCall<ReqT, RespT> call,
30
Metadata metadata);
31
}
32
```
33
34
**Usage Example:**
35
36
```java
37
import io.quarkus.grpc.ExceptionHandler;
38
import io.grpc.Status;
39
import io.grpc.StatusRuntimeException;
40
41
public class CustomExceptionHandler<ReqT, RespT> extends ExceptionHandler<ReqT, RespT> {
42
43
public CustomExceptionHandler(ServerCall.Listener<ReqT> listener,
44
ServerCall<ReqT, RespT> call,
45
Metadata metadata) {
46
super(listener, call, metadata);
47
}
48
49
@Override
50
protected void handleException(Throwable t,
51
ServerCall<ReqT, RespT> call,
52
Metadata metadata) {
53
Status status;
54
55
if (t instanceof ValidationException) {
56
status = Status.INVALID_ARGUMENT.withDescription(t.getMessage());
57
} else if (t instanceof NotFoundException) {
58
status = Status.NOT_FOUND.withDescription(t.getMessage());
59
} else if (t instanceof UnauthorizedException) {
60
status = Status.UNAUTHENTICATED.withDescription(t.getMessage());
61
} else if (t instanceof ForbiddenException) {
62
status = Status.PERMISSION_DENIED.withDescription(t.getMessage());
63
} else {
64
status = Status.INTERNAL.withDescription("Internal server error");
65
}
66
67
call.close(status, metadata);
68
}
69
}
70
```
71
72
### ExceptionHandlerProvider Interface
73
74
Provider for creating ExceptionHandler instances. Implement this interface and expose it as a CDI bean to customize exception handling across all gRPC services.
75
76
```java { .api }
77
/**
78
* Provider for ExceptionHandler.
79
*
80
* To use a custom ExceptionHandler, extend {@link ExceptionHandler} and implement
81
* an {@link ExceptionHandlerProvider}, and expose it as a CDI bean.
82
*/
83
public interface ExceptionHandlerProvider {
84
<ReqT, RespT> ExceptionHandler<ReqT, RespT> createHandler(
85
Listener<ReqT> listener,
86
ServerCall<ReqT, RespT> serverCall,
87
Metadata metadata
88
);
89
90
default Throwable transform(Throwable t) {
91
return toStatusException(t, false); // previous default was false
92
}
93
94
/**
95
* Throw Status exception.
96
*
97
* @param t the throwable to transform
98
* @param runtime true if we should throw StatusRuntimeException, false for StatusException
99
* @return Status(Runtime)Exception
100
*/
101
static Exception toStatusException(Throwable t, boolean runtime);
102
103
/**
104
* Get Status from exception.
105
*
106
* @param t the throwable to read or create status from
107
* @return gRPC Status instance
108
*/
109
static Status toStatus(Throwable t);
110
111
/**
112
* Get optional Metadata from exception.
113
*
114
* @param t the throwable to read or create metadata from
115
* @return optional gRPC Metadata instance
116
*/
117
static Optional<Metadata> toTrailers(Throwable t);
118
}
119
```
120
121
**Usage Examples:**
122
123
```java
124
import io.quarkus.grpc.ExceptionHandlerProvider;
125
import io.quarkus.grpc.ExceptionHandler;
126
import jakarta.enterprise.context.ApplicationScoped;
127
128
@ApplicationScoped
129
public class CustomExceptionHandlerProvider implements ExceptionHandlerProvider {
130
131
@Override
132
public <ReqT, RespT> ExceptionHandler<ReqT, RespT> createHandler(
133
Listener<ReqT> listener,
134
ServerCall<ReqT, RespT> serverCall,
135
Metadata metadata) {
136
137
return new CustomExceptionHandler<>(listener, serverCall, metadata);
138
}
139
140
@Override
141
public Throwable transform(Throwable t) {
142
// Custom transformation logic
143
if (t instanceof BusinessException) {
144
BusinessException be = (BusinessException) t;
145
Status status = Status.INVALID_ARGUMENT
146
.withDescription(be.getMessage())
147
.withCause(be);
148
return status.asRuntimeException();
149
}
150
151
// Use default transformation for other exceptions
152
return ExceptionHandlerProvider.toStatusException(t, true);
153
}
154
}
155
```
156
157
### Status Utility Methods
158
159
The ExceptionHandlerProvider interface provides static utility methods for exception transformation:
160
161
```java { .api }
162
/**
163
* Throw Status exception.
164
*/
165
static Exception toStatusException(Throwable t, boolean runtime) {
166
// Converts any throwable to StatusException or StatusRuntimeException
167
}
168
169
/**
170
* Get Status from exception.
171
*/
172
static Status toStatus(Throwable t) {
173
// Extracts or creates gRPC Status from throwable
174
}
175
176
/**
177
* Get optional Metadata from exception.
178
*/
179
static Optional<Metadata> toTrailers(Throwable t) {
180
// Extracts optional Metadata trailers from exception
181
}
182
```
183
184
**Usage Examples:**
185
186
```java
187
import io.quarkus.grpc.ExceptionHandlerProvider;
188
import io.grpc.Status;
189
import io.grpc.Metadata;
190
191
public class ExceptionUtils {
192
193
public static void demonstrateStatusConversion() {
194
try {
195
riskyOperation();
196
} catch (Exception e) {
197
// Convert to gRPC status
198
Status status = ExceptionHandlerProvider.toStatus(e);
199
System.out.println("Status code: " + status.getCode());
200
System.out.println("Description: " + status.getDescription());
201
202
// Get trailers if available
203
Optional<Metadata> trailers = ExceptionHandlerProvider.toTrailers(e);
204
if (trailers.isPresent()) {
205
System.out.println("Has trailers: " + trailers.get().keys());
206
}
207
208
// Convert to StatusRuntimeException
209
Exception statusException = ExceptionHandlerProvider.toStatusException(e, true);
210
throw (StatusRuntimeException) statusException;
211
}
212
}
213
}
214
```
215
216
## Common Exception Handling Patterns
217
218
### Business Logic Exception Mapping
219
220
```java
221
@ApplicationScoped
222
public class BusinessExceptionHandler implements ExceptionHandlerProvider {
223
224
@Override
225
public <ReqT, RespT> ExceptionHandler<ReqT, RespT> createHandler(
226
Listener<ReqT> listener,
227
ServerCall<ReqT, RespT> serverCall,
228
Metadata metadata) {
229
230
return new ExceptionHandler<ReqT, RespT>(listener, serverCall, metadata) {
231
@Override
232
protected void handleException(Throwable t,
233
ServerCall<ReqT, RespT> call,
234
Metadata metadata) {
235
236
Status status = mapToGrpcStatus(t);
237
Metadata responseMetadata = createResponseMetadata(t, metadata);
238
239
call.close(status, responseMetadata);
240
}
241
};
242
}
243
244
private Status mapToGrpcStatus(Throwable t) {
245
if (t instanceof ValidationException) {
246
ValidationException ve = (ValidationException) t;
247
return Status.INVALID_ARGUMENT
248
.withDescription("Validation failed: " + ve.getMessage())
249
.withCause(ve);
250
}
251
252
if (t instanceof ResourceNotFoundException) {
253
return Status.NOT_FOUND
254
.withDescription("Resource not found: " + t.getMessage());
255
}
256
257
if (t instanceof InsufficientPermissionsException) {
258
return Status.PERMISSION_DENIED
259
.withDescription("Access denied: " + t.getMessage());
260
}
261
262
if (t instanceof RateLimitExceededException) {
263
return Status.RESOURCE_EXHAUSTED
264
.withDescription("Rate limit exceeded");
265
}
266
267
// Default for unexpected exceptions
268
return Status.INTERNAL
269
.withDescription("Internal server error")
270
.withCause(t);
271
}
272
273
private Metadata createResponseMetadata(Throwable t, Metadata requestMetadata) {
274
Metadata responseMetadata = new Metadata();
275
276
// Add correlation ID if present in request
277
Key<String> correlationKey = Key.of("correlation-id", Metadata.ASCII_STRING_MARSHALLER);
278
String correlationId = requestMetadata.get(correlationKey);
279
if (correlationId != null) {
280
responseMetadata.put(correlationKey, correlationId);
281
}
282
283
// Add error details for certain exception types
284
if (t instanceof ValidationException) {
285
Key<String> errorDetailsKey = Key.of("error-details", Metadata.ASCII_STRING_MARSHALLER);
286
ValidationException ve = (ValidationException) t;
287
responseMetadata.put(errorDetailsKey, ve.getValidationErrors().toString());
288
}
289
290
return responseMetadata;
291
}
292
}
293
```
294
295
### Structured Error Response
296
297
```java
298
public class StructuredErrorHandler implements ExceptionHandlerProvider {
299
300
@Override
301
public Throwable transform(Throwable t) {
302
ErrorInfo errorInfo = createErrorInfo(t);
303
304
Status status = Status.fromCode(errorInfo.getStatusCode())
305
.withDescription(errorInfo.getMessage());
306
307
Metadata metadata = new Metadata();
308
Key<String> errorCodeKey = Key.of("error-code", Metadata.ASCII_STRING_MARSHALLER);
309
metadata.put(errorCodeKey, errorInfo.getErrorCode());
310
311
if (errorInfo.hasDetails()) {
312
Key<String> detailsKey = Key.of("error-details", Metadata.ASCII_STRING_MARSHALLER);
313
metadata.put(detailsKey, errorInfo.getDetailsJson());
314
}
315
316
return status.asRuntimeException(metadata);
317
}
318
319
private ErrorInfo createErrorInfo(Throwable t) {
320
if (t instanceof ValidationException) {
321
ValidationException ve = (ValidationException) t;
322
return ErrorInfo.builder()
323
.statusCode(Status.Code.INVALID_ARGUMENT)
324
.errorCode("VALIDATION_FAILED")
325
.message(ve.getMessage())
326
.details(ve.getValidationErrors())
327
.build();
328
}
329
330
if (t instanceof ResourceNotFoundException) {
331
return ErrorInfo.builder()
332
.statusCode(Status.Code.NOT_FOUND)
333
.errorCode("RESOURCE_NOT_FOUND")
334
.message(t.getMessage())
335
.build();
336
}
337
338
// Default error info
339
return ErrorInfo.builder()
340
.statusCode(Status.Code.INTERNAL)
341
.errorCode("INTERNAL_ERROR")
342
.message("An unexpected error occurred")
343
.build();
344
}
345
}
346
```
347
348
### Reactive Exception Handling
349
350
In gRPC services that use Mutiny, exceptions can be handled reactively:
351
352
```java
353
@GrpcService
354
public class ReactiveServiceWithErrorHandling implements MutinyService {
355
356
public Uni<UserResponse> getUser(UserRequest request) {
357
return validateRequest(request)
358
.onItem().transformToUni(this::findUser)
359
.onFailure(ValidationException.class).transform(ex ->
360
new StatusRuntimeException(
361
Status.INVALID_ARGUMENT.withDescription(ex.getMessage())))
362
.onFailure(UserNotFoundException.class).transform(ex ->
363
new StatusRuntimeException(
364
Status.NOT_FOUND.withDescription("User not found")))
365
.onFailure().transform(ex -> {
366
if (ex instanceof StatusRuntimeException) {
367
return ex; // Already a gRPC exception
368
}
369
return new StatusRuntimeException(
370
Status.INTERNAL.withDescription("Unexpected error"));
371
});
372
}
373
374
private Uni<UserRequest> validateRequest(UserRequest request) {
375
if (request.getUserId().isEmpty()) {
376
return Uni.createFrom().failure(
377
new ValidationException("User ID is required"));
378
}
379
return Uni.createFrom().item(request);
380
}
381
}
382
```
383
384
## Exception Handling Best Practices
385
386
1. **Map business exceptions to appropriate gRPC status codes**
387
2. **Preserve error context in status descriptions**
388
3. **Use metadata for structured error information**
389
4. **Log exceptions appropriately (don't expose sensitive details)**
390
5. **Provide correlation IDs for error tracking**
391
6. **Use reactive error handling patterns in Mutiny services**
392
7. **Consider circuit breaker patterns for external service failures**