EzyHTTP Handle Exceptions
Updated at 1783007689000EzyHTTP provides three common ways to handle exceptions in your application:
- throw a built-in HTTP exception
- handle an exception inside a controller
- handle exceptions in a global exception handler
The right choice depends on the scope of the error. If the error maps directly to an HTTP status code, use a built-in exception. If it belongs only to one controller, handle it in that controller. If it is shared by many controllers, move it to a global handler.
flowchart TD
A[Request handler throws exception] --> B{Controller has @TryCatch?}
B -->|Yes| C[Use controller-level handler]
B -->|No| D{Global @ExceptionHandler matches?}
D -->|Yes| E[Use global handler]
D -->|No| F{Is HttpRequestException?}
F -->|Yes| G[Return exception status code and data]
F -->|No| H[Return internal server error or custom unhandled error response]
Use Default Exceptions
EzyHTTP provides several built-in HTTP exceptions that already map to common HTTP status codes:
-
HttpBadRequestException:400 Bad Request -
HttpUnauthorizedException:401 Unauthorized -
HttpPaymentRequiredException:402 Payment Required -
HttpForbiddenException:403 Forbidden -
HttpNotFoundException:404 Not Found -
HttpMethodNotAllowedException:405 Method Not Allowed -
HttpNotAcceptableException:406 Not Acceptable -
HttpRequestTimeoutException:408 Request Timeout -
HttpConflictException:409 Conflict -
HttpUnsupportedMediaTypeException:415 Unsupported Media Type -
HttpTooManyRequestsException:429 Too Many Requests -
HttpInternalServerErrorException:500 Internal Server Error
All of these exceptions extend
HttpRequestException. When EzyHTTP catches a HttpRequestException, it uses the exception code as the HTTP response status and the exception data as the response body.
For example:
public void validate(AddBookRequest request) { if (request.getBookName().length() > MAX_NAME_LENGTH) { throw new HttpBadRequestException( "name length must < " + MAX_NAME_LENGTH ); } }
In this case, EzyHTTP returns:
- status:
400 Bad Request - body:
"name length must < {MAX_NAME_LENGTH}"
If you need a status code that does not have a dedicated exception class, you can throw
HttpRequestException directly:
throw new HttpRequestException(422, Map.of( "bookName", "invalid" ));
The first argument is the HTTP status code. The second argument is the response body. It can be a string, a map, a DTO, or any object that your response converter can serialize.
If the exception data is
null, EzyHTTP returns an empty response body object instead of exposing a Java exception message.Handle Exceptions In A Controller
Use a controller-level
@TryCatch method when the exception belongs only to that controller.@Controller("/api/v1/category") public class CategoryController { @DoGet("/{id}") public Category getCategory(@PathVariable long id) { return categoryService.getCategory(id); } @TryCatch(CategoryNotFoundException.class) public ResponseEntity handle(CategoryNotFoundException e) { return ResponseEntity.notFound( Collections.singletonMap("category", "notFound") ); } }
This keeps local rules close to the endpoint that owns them. It is useful when the same exception should produce a special response only in one controller.
@TryCatch also supports multiple exception classes:@TryCatch({ IllegalStateException.class, NullPointerException.class }) public ResponseEntity handleInvalidState(Exception e) { return ResponseEntity.badRequest(e.getMessage()); }
A handler method can return a normal object or a
ResponseEntity. Use ResponseEntity when you need to control the status code, headers, content type, or body explicitly.@TryCatch(NoPermissionException.class) public ResponseEntity handleNoPermission(NoPermissionException e) { return ResponseEntity.builder() .status(403) .body(Map.of("permission", "denied")) .build(); }
Handle Exceptions In A Global Handler
Use a global handler when the same exception can happen in many controllers.
@ExceptionHandler public class GlobalExceptionHandler extends EzyLoggable { @TryCatch(IllegalArgumentException.class) public ResponseEntity handleException( IllegalArgumentException e ) { logger.info("invalid argument: {}", e.getMessage()); return ResponseEntity.badRequest(e.getMessage()); } @TryCatch(NotFoundException.class) public ResponseEntity handleException( NotFoundException e ) { logger.info("not found: {}", e.getMessage()); return ResponseEntity.notFound(e.getMessage()); } }
A global handler is a good place for application-wide policies, for example:
- validation errors
- authorization errors
- business rule errors
- resource-not-found errors
- shared error response format
For example, you can make all validation errors return the same JSON structure:
@ExceptionHandler public class GlobalExceptionHandler { @TryCatch(IllegalArgumentException.class) public ResponseEntity handleBadRequest(IllegalArgumentException e) { return ResponseEntity.badRequest(Map.of( "error", "badRequest", "message", e.getMessage() )); } }
Choosing The Right Approach
Use built-in HTTP exceptions when the exception is already an HTTP response decision:
throw new HttpNotFoundException("book not found");
Use controller-level
@TryCatch when the response is specific to one controller:@TryCatch(CategoryNotFoundException.class) public ResponseEntity handle(CategoryNotFoundException e) { return ResponseEntity.notFound(Map.of("category", "notFound")); }
Use a global
@ExceptionHandler when the same exception handling rule should be reused across controllers:@ExceptionHandler public class GlobalExceptionHandler { @TryCatch(NotFoundException.class) public ResponseEntity handle(NotFoundException e) { return ResponseEntity.notFound(Map.of( "error", "notFound", "message", e.getMessage() )); } }
ResponseEntity
ResponseEntity is useful when the handler needs to return more than a plain body.Common helpers include:
ResponseEntity.ok(); ResponseEntity.ok(body); ResponseEntity.badRequest(); ResponseEntity.badRequest(body); ResponseEntity.notFound(); ResponseEntity.notFound(body); ResponseEntity.noContent();
For custom status codes, headers, or content type, use the builder:
return ResponseEntity.builder() .status(409) .header("X-Error-Code", "BOOK_ALREADY_EXISTS") .body(Map.of( "error", "conflict", "message", "book already exists" )) .build();
Best Practices
Use clear exception types for business errors. For example, prefer
BookNotFoundException or NoPermissionException over throwing a generic RuntimeException.Keep controller-level handlers small. If the same handler appears in multiple controllers, move it to a global handler.
Return consistent error bodies. A simple structure like this is easier for clients to consume:
Map.of(
"error", "notFound",
"message", "book not found"
)
Use
HttpRequestException or its subclasses when you already know the HTTP status at the place where the error is detected.Use global handlers when you want to hide internal exception details and expose a stable API contract.
Next
You can continue with request interceptors to learn how to intercept client requests before they reach your controllers.