Locale independent error handling

Kiril Nugmanov
Zenitech
Published in
3 min readMay 7, 2020
Clean code in a nutshell: 5 Reasons you Should be Writing Clean Code

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:

  1. System must be alerted — in a simple way, the error just needs to be thrown
  2. Collect of diagnostic data — information that would help to diagnose the origin of the error
  3. Identification of the root cause of the error
  4. 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 error
  • values - 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.

--

--