Locale independent error handling
Q. How to handle errors in modern JAVA applications?
A. There is no single answer. But let’s dive into the possible options…
Errors vs Business rules
From time to time, we see a mixture of validation rules and exceptions throwing in a code base. But then again, what is a validation rule? It’s an ordinal response from an application. Hence, it is not an error and should not be treated as such. From a development point of view, errors must be flagged (thrown), while violations of business rules is ordinary data flow
What is an error?
An error is specific a application state in which following actions must be taken:
- System must be alerted — in a simple way, the error just needs to be
thrown
- Collect of diagnostic data — information that would help to diagnose the origin of the error
- Identification of the root cause of the error
- Handling the error — that is the tricky part. How should errors be handled?
Flagging an error
To alert an application about an error in JAVA we can use exceptions
(and throw
them). Currently, there are two types of exceptions:
- checked exceptions
- unchecked exceptions
Use checked exceptions for recoverable conditions and runtime exceptions for programming errors.
“Effective Java. 2nd edition”, Item 58, Joshua Bloch
In 99.99% of the cases, all errors are unrecoverable. Everything else is up to you and how you decide to proceed.
Diagnostic data
On throwing an exception some diagnostic data already in place:
- stack trace
- error line
- error message
But what if we need i18n
and/or l10n
in our application?
Exception model
To address i18n
and/or l10n
in an application we need to use error codes and contextual data:
package org.cynic.app.domain;import java.io.Serializable;
import java.util.Arrays;public class ApplicationException extends RuntimeException {
private final String code;
private final Serializable[] values; public ApplicationException( String code, Serializable... values) {
this(null, code, values);
} public ApplicationException(Throwable cause, String code, Serializable... values) {
super(String.join(" ", code, Arrays.toString(values)));
initCause(cause); this.code = code;
this.values = values;
} public String getCode() {
return code;
} public Serializable[] getValues() {
return values;
}
}
In this sample we have:
code
- error code which could be translated into a localized version of the errorvalues
- optional context data for specifying error message more accurately
Example of usage
Our sample application looks like this:
package org.cynic.app;import org.cynic.app.domain.ApplicationException;public class Application {
public static void main(String[] args) {
throw new ApplicationException(new Exception("Error"), "error.id.not-found", "id-field", 10);
}
}
And the output to console while handling exception will be the following:
Exception in thread "main" org.cynic.app.domain.ApplicationException: error.id.not-found [id-field, 10]
at org.cynic.app.Application.main(Application.java:7)
Caused by: java.lang.Exception: Error
... 1 more
As we see, the exception message is lean and neat. The root cause is provided in the stack trace.
Localization of errors
In case the error message should be translated to human readable format — it’s a good practice to use properties files. Spring framework provides it’s own implementation (org.springframework.context.MessageSource
) which will be used in example.
package org.cynic.app;import org.cynic.app.domain.ApplicationException;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;import java.util.Locale;public class Application {
private static MessageSource messageSource = new ResourceBundleMessageSource() {{
setBasename("application");
}}; public static void main(String[] args) {
try {
throw new ApplicationException(new Exception("Banana"), "error.id.not-found", "id-field", 10);
} catch (ApplicationException e) {
System.out.println(messageSource.getMessage(e.getCode(), e.getValues(), Locale.getDefault()));
}
}
}
Example of application.properties
file with error message
error.id.not-found=Field "{0}" with ID "{1}" not found.
Output of executed application to console:
Field "id-field" with ID "10" not found.
As we see message was transformed from technical level (error.id.not-found [id-field, 10]
) to human readable — error.id.not-found=Field "{0}" with ID "{1}" not found
. Such approach allowed to display localized error messages and not loose technical part of it.
Real life example
In real life application such technique could be combined with @ControllerAdvice
annotation and handle error globally (on controller level).
package org.cynic.app.controller;
import org.cynic.app.domain.ApplicationException;
import org.springframework.context.MessageSource;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.LocaleResolver;
@ControllerAdvice
public class GlobalErrorHandler {
private final MessageSource messageSource;
private final LocaleResolver localeResolver;
public GlobalErrorHandler(MessageSource messageSource, LocaleResolver localeResolver) {
this.messageSource = messageSource;
this.localeResolver = localeResolver;
}
@ExceptionHandler(ApplicationException.class)
public String applicationException(ApplicationException exception, HttpServletRequest httpServletRequest) {
return messageSource.getMessage(exception.getCode(), exception.getValues(), localeResolver.resolveLocale(httpServletRequest));
}
@ExceptionHandler(Exception.class)
public String applicationException(Exception exception, HttpServletRequest httpServletRequest) {
return messageSource.getMessage("error.unknown", new Object[]{exception.getMessage()}, localeResolver.resolveLocale(httpServletRequest));
}
}
There is no simple way to handle errors. However, life could be much simpler if some issues, like i18n
, would be addressed in early stages of application development.