Exception handling is a cross-cutting concern, should be kept separate from business logic and applied declaratively.
A common practice is to create some custom exception classes like some ServiceException and errors code enums, wherein each instance of error code enum represents an error scenario. An exception class could be either checked or unchecked, but handling of exception is no different. For almost all error scenarios unchecked exception can serve the purpose really well, saving developers from explicitly writing
try
catch
blocks and
throws
clauses. Though not recommended but limited checked exceptions can be created and thrown from methods where calling programs can take some recovery measures.
Standard way of handling exceptions in Spring is
@ControllerAdvice
using AOP, following the same principles
spring-boot-problem-handler
makes available everything related to exception handling for both
Spring Web
(Servlet) and
Spring Webflux
(Reactive) Rest applications, so there is no need to define any custom exceptions or custom
ControllerAdvice
advices into consumer application, all can be done with zero custom code but by specifying error details in
properties
file.
<dependency>
<groupId>io.github.officiallysingh</groupId>
<artifactId>spring-boot-problem-handler</artifactId>
<version>${spring-boot-problem-handler.version}</version>
</dependency>
It does all hard part, A lot of advices are out of box available which are autoconfigured as
ControllerAdvice
‘s depending on the jars in classpath of consumer application.
Even for exceptions for which no advices are defined
, respective error response can be specified by messages in
properties
file, elaborated in
Usage
section. New custom advices could be required only in cases where it is required to take some data from exception instance to dynamically derive
Error key
or to use this data to resolve any placeholders in error message. In such cases consumer application can define their own custom
ControllerAdvice
‘s, Any existing advice can be referred to weave the custom advice into the framework.
A default set of
ControllerAdvice
‘s are always configured irrespective of the fact that whether the application is Spring Web or Spring Webflux, but few advices are conditional such as for Handling Security, OpenAPI and Dao related exceptions, which are elaborated in their respective sections.
Specify message source bundles as follows. Make sure to include
i18/problems
bundled in the library, as it has default messages for certain exception. And it should be last in the list of
basenames
, so that it has lowest priority and any default messages coming from
problems.properties
can be overridden by specifying the property with different value in application’s
errors.properties
spring.messages.basename=i18n/errors,i18/problems
spring.messages.use-code-as-default-message=true
If
use-code-as-default-message
is set to
false
and the message is not found in any of the
properties
file then it will throw
NoSuchMessageException
complaining that no message is found for given code. So if it is intended to enforce all messages for exceptions to be specified in
properties
file, set it to
false
, but not recommended.
To be on safer side, it’s recommended to keep it
true
, in that case if some message is not found, the message key is taken as its value, which can be updated later into
properties
file, once noticed.
problem.enabled=true
problem.type-url=http://localhost:8080/problems/help.html
problem.debug-enabled=false
problem.stacktrace-enabled=false
problem.cause-chains-enabled=false
#problem.jackson-module-enabled=false
#problem.dao-advice-enabled=false
#problem.security-advice-enabled=false
problem.open-api.path=/oas/api.json
problem.open-api.exclude-patterns=/api/states,/api/states/**,/api/employees,/api/employees/**,/problems/**
problem.open-api.req-validation-enabled=true
problem.open-api.res-validation-enabled=false
problem.enabled
: To enable or disable autoconfigurations, default is
true
.In case consumer applications are interested to avail advices but want full control over configurations, then it can be set to
false
and required advices can be configured as Spring beans similar to how they are autoconfigured.
problem.type-url
: The base
URL
for
Help page
describing errors. For different exceptions respective code for exception is appended to it followed by a
#
problem.debug-enabled
: To enable or disable debugging i.e. to get the message resolvers to specify the error messages in
properties
files. Elaborated in
Usage
section. Default is
false
.
problem.stacktrace-enabled
: To enable or disable Stacktraces, default is
false
. Should only be set to
true
for debugging purposes only on local or lower environments, otherwise the application internals may be exposed.
problem.cause-chains-enabled
: To enable or disable cause chains, default is
false
. Elaborated in
Usage
section.
problem.jackson-module-enabled
: To enable or disable Jackson Problem Module autoconfiguration, default is
true
. Set it to
false
in case consumer application need to define Serialization/Deserialization explicitly. Or if
Gson
is to be used instead of
Jackson
. If disabled the required serializers need to be defined by consumer application.
problem.dao-advice-enabled
: To enable or disable Dao advice autoconfiguration, default is
true
. Set it to
false
in case consumer application need to define Dao advice configurations explicitly.
problem.security-advice-enabled
: To enable or disable Security advice autoconfiguration, default is
true
. Set it to
false
in case consumer application need to define Security advice configurations explicitly.
problem.open-api.path
: OpenAPI Specification path. Ideally should be in classpath and start with
/
. If not specified, OpenAPI Specification validation is not enabled.
problem.open-api.exclude-patterns
: List of
URI
Ant patterns to be excluded from OpenAPI specification validation. Default is empty.
problem.open-api.req-validation-enabled
: To enable or disable OpenAPI specification validation for request, default is
false
.
problem.open-api.res-validation-enabled
: To enable or disable OpenAPI specification validation for response, default is
false
.
"type":"http://localhost:8080/problems/help.html#XYZ-001",
"title":"Internal Server Error",
"status":500,
"detail":"A job instance already exists and is complete for parameters={'date':'{value=2023-08-13, type=class java.time.LocalDate, identifying=true}'}. If you want to run this job again, change the parameters.",
"instance":"/api/myjob",
"method":"PUT",
"timestamp":"2023-08-14T20:45:45.737227+05:30",
"code":"XYZ-001"
Response Header when service is configured for Json
HttpMessageConverters
content-type: application/problem+json
Response Header when service is configured for XML
HttpMessageConverters
content-type: application/problem+xml
Description
"type":"http://localhost:8080/problems/help.html#XYZ-001",
"title":"Internal Server Error",
"status":500,
"detail":"A job instance already exists and is complete for parameters={'date':'{value=2023-08-13, type=class java.time.LocalDate, identifying=true}'}. If you want to run this job again, change the parameters.",
"instance":"/api/myjob",
"method":"PUT",
"timestamp":"2023-08-14T20:51:43.993249+05:30",
"code":"XYZ-001",
"codeResolver":{
"codes":[
"code.org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException"
"defaultMessage":"500",
"arguments":null
"titleResolver":{
"codes":[
"title.org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException"
"defaultMessage":"Internal Server Error",
"arguments":null
"detailResolver":{
"codes":[
"detail.org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException"
"defaultMessage":"A job instance already exists and is complete for parameters={'date':'{value=2023-08-13, type=class java.time.LocalDate, identifying=true}'}. If you want to run this job again, change the parameters.",
"arguments":null
"statusResolver":{
"codes":[
"status.org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException"
"defaultMessage":"500",
"arguments":null
Respective codes for corresponding attribute can be copied and message can be specified for same in
properties
file.
NOTE
:
org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException
i.e. fully qualified name of exception is the
Error key
in above case.
This scenario also covers all the exceptions for which advices are not defined
. But additionally
HttpStatus
need to be specified in
properties
file as it has not been specified anywhere in code because
ControllerAdvice
is not defined, if status not given even in
properties
file
HttpStatus.INTERNAL_SERVER_ERROR
is taken as default.
Hence the error response can be specified as follows.
status.org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException=409
code.org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException=Some code
title.org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException=Some title
detail.org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException=Some message details
To minimize the number of properties following defaults are taken if
HttpStatus
is specified as
status
.<error key> property.
Apart from exceptions thrown by frameworks or java, every application need to throw custom exceptions.
ApplicationProblem
and
ApplicationException
classes are available in the library to throw an unchecked or checked exception respectively.
Problems
is the central static helper class to create Problem instances and throw either checked or unchecked exceptions
, as demonstrated below. It provides multiple fluent methods to build and throw exceptions.
The simplistic way is to just specify a unique error key and
HttpStatus
.
throw Problems.newInstance("sample.problem").throwAble(HttpStatus.EXPECTATION_FAILED);
Error response attributes
code
,
title
and
detail
are expected from the message source (
properties
file) available as follows.
Notice the
Error key
s
ample.problem
in following properties.
code.sample.problem=AYX123
title.sample.problem=Some title
detail.sample.problem=Some message details
But exceptions come with some default attributes as follows, to minimize the number of properties required to be defined in
properties
file
If the messages are not found in
properties
files, defaults are taken as follows.
throw Problems.newInstance("sample.problem")
.defaultDetail("Default details if not found in properties file with parma1: {0} and param2: {1}")
.detailArgs("P1", "P2")
.cause(new IllegalStateException("Artificially induced illegal state"))
.throwAble(HttpStatus.EXPECTATION_FAILED); // .throwAbleChecked(HttpStatus.EXPECTATION_FAILED)
The above code snippet would throw unchecked exception, though not recommended but to throw checked exception,
use
throwAbleChecked
as terminal operation as highlighted in java comment above.
The attributes corresponding to error key
sample.problem
can be provided in
properties
file as follows.
code.sample.problem=404
title.sample.problem=Some title
detail.sample.problem=Some details with param one: {0} and param other: {1}
Sometimes it is not desirable to throw exceptions as they occur, but to collect them to throw at a later point in execution.
Or to throw multiple exceptions together. That can be done as follows.
Problem problemOne = Problems.newInstance("sample.problem.one").get();
Problem problemTwo = Problems.newInstance("sample.problem.two").get();
throw Problems.throwAble(HttpStatus.MULTI_STATUS, problemOne, problemTwo);
HttpStatus
can also be set over custom exception as follows, the same would reflect in error response and other error attributes default would be derived by given
HttpStatus
attribute in
@ResponseStatus
@ResponseStatus(HttpStatus.NOT_IMPLEMENTED)
private static final class MyException extends RuntimeException {
public MyException() {
public MyException(final Throwable cause) {
super(cause);
"type":"http://localhost:8080/problems/help.html#XYZ-001",
"title":"Internal Server Error",
"status":500,
"detail":"A job instance already exists and is complete for parameters={'date':'{value=2023-08-13, type=class java.time.LocalDate, identifying=true}'}. If you want to run this job again, change the parameters.",
"instance":"/api/myjob",
"method":"PUT",
"timestamp":"2023-08-14T21:01:56.378749+05:30",
"code":"XYZ-001",
"statcktrace":[
"org.springframework.batch.core.repository.support.SimpleJobRepository.createJobExecution(SimpleJobRepository.java:159)",
"java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)",
"java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)",
"java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)",
"java.base/java.lang.reflect.Method.invoke(Method.java:568)",
".......",
"..............",
"org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)",
"org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)",
"java.base/java.lang.Thread.run(Thread.java:833)"
"type":"http://localhost:8080/problems/help.html#XYZ-001",
"title":"Not Implemented",
"status":501,
"detail":"expected",
"instance":"/problems/handler-throwable-annotated-cause",
"method":"GET",
"timestamp":"2023-08-14T22:09:56.284473+05:30",
"code":"XYZ-001",
"cause":{
"code":"501",
"title":"Not Implemented",
"detail":"Something has gone wrong",
"cause":{
"code":"501",
"title":"Not Implemented"
@AllArgsConstructor(staticName = "of")
public class CustomErrorResponse {
private HttpStatus status;
private String message;
@Component
class CustomErrorResponseBuilder implements ErrorResponseBuilder<NativeWebRequest, ResponseEntity<CustomErrorResponse>> {
@Override
public ResponseEntity<CustomErrorResponse> buildResponse(final Throwable throwable, final NativeWebRequest request,
final HttpStatus status, final HttpHeaders headers, final Problem problem) {
CustomErrorResponse errorResponse = CustomErrorResponse.of(status, problem.getDetail());
ResponseEntity<CustomErrorResponse> responseEntity = ResponseEntity
.status(status).headers(headers).contentType(MediaTypes.PROBLEM).body(errorResponse);
return responseEntity;
For Spring Webflux applications
@Component
class CustomErrorResponseBuilder implements ErrorResponseBuilder<ServerWebExchange, Mono<ResponseEntity<CustomErrorResponse>>> {
@Override
public Mono<ResponseEntity<CustomErrorResponse>> buildResponse(final Throwable throwable, final ServerWebExchange request,
final HttpStatus status, final HttpHeaders headers, final Problem problem) {
CustomErrorResponse errorResponse = CustomErrorResponse.of(status, problem.getDetail());
ResponseEntity<CustomErrorResponse> responseEntity = ResponseEntity
.status(status).headers(headers).contentType(MediaTypes.PROBLEM).body(errorResponse);
return Mono.just(responseEntity);
Customize or Override advices
Any autoconfigured advice can be customized by overriding the same and providing a different implementation. Make sure to add annotation @Order(Ordered.HIGHEST_PRECEDENCE)
over the class, It makes this handler to take precedence over the fallback advice which handles Throwable
i.e. for all exceptions for which no ControllerAdvice
s are defined.
For Spring Web applications
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // Important to note
class CustomMethodArgumentNotValidExceptionHandler implements MethodArgumentNotValidAdviceTrait<NativeWebRequest, ResponseEntity<ProblemDetail>> {
public ResponseEntity<ProblemDetail> handleMethodArgumentNotValid(final MethodArgumentNotValidException exception, final NativeWebRequest request) {
List<String> violations = processBindingResult(exception.getBindingResult());
final String errors = violations.stream()
.collect(Collectors.joining(", "));
Problem problem = Problem.code(ProblemUtils.statusCode(HttpStatus.BAD_REQUEST)).title(HttpStatus.BAD_REQUEST.getReasonPhrase())
.detail(errors).build();
return create(exception, request, HttpStatus.BAD_REQUEST,
problem);
List<String> processBindingResult(final BindingResult bindingResult) {
final List<String> fieldErrors =
bindingResult.getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.toList();
final List<String> globalErrors =
bindingResult.getGlobalErrors().stream()
.map(
objectError ->
objectError.getObjectName() + ": " + objectError.getDefaultMessage())
.toList();
final List<String> errors = new ArrayList<>();
if (CollectionUtils.isNotEmpty(fieldErrors)) {
errors.addAll(fieldErrors);
if (CollectionUtils.isNotEmpty(globalErrors)) {
errors.addAll(globalErrors);
return errors;
For Spring Webflux applications
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // Important to note
class CustomMethodArgumentNotValidExceptionHandler implements MethodArgumentNotValidAdviceTrait<ServerWebExchange, Mono<ResponseEntity<ProblemDetail>>> {
public Mono<ResponseEntity<ProblemDetail>> handleMethodArgumentNotValid(final MethodArgumentNotValidException exception, final ServerWebExchange request) {
// It remains the same as implemented for Spring web, above
There should not be any need to create any custom exception hence new advices, but if there is a pressing need to do so, custom exception can be created and corresponding custom ControllerAdvice
can be defined for the same, though not recommended. Following example demonstrates a new advice for some custom exception MyCustomException
.
For Spring Web applications
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // Important to note
public class MyCustomAdvice implements AdviceTrait<NativeWebRequest, ResponseEntity<ProblemDetail>> {
@ExceptionHandler
public ResponseEntity<ProblemDetail> handleMyCustomException(final MyCustomException exception, final NativeWebRequest request) {
// Custome logic to set the error response
Problem problem = Problem.code(String.valueOf(HttpStatus.BAD_REQUEST.value())).title(HttpStatus.BAD_REQUEST.getReasonPhrase())
.detail(exception.getMessage).build();
return create(exception, request, HttpStatus.BAD_REQUEST,
problem);
For Spring Webflux applications
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // Important to note
public class MyCustomAdvice implements AdviceTrait<ServerWebExchange, Mono<ResponseEntity<ProblemDetail>>> {
@ExceptionHandler
public Mono<ResponseEntity<ProblemDetail>> handleMyCustomException(final MyCustomException exception, final ServerWebExchange request) {
// It remains the same as implemented for Spring web, above
We accelerate your business transformation by leveraging best fit CLOUD NATIVE technologies wherever feasible.
We are DIGITAL consultants who partner with you to solve & deliver.
©2024 StatusNeo Technology Consulting Pvt. Ltd. & its Global Affiliates including StatusNeo Inc.
All Rights Reserved