CodeGym /Courses /Module 5. Spring /Crafting user-friendly error messages

Crafting user-friendly error messages

Module 5. Spring
Level 8 , Lesson 7
Available

Imagine you're filling out a form on a website and the system replies: "Error 400. Bad request. Field user_input does not meet validation requirements." Feel that cold, robotic tone? It's hard for a user to understand what went wrong and how to fix it.

Well-crafted error messages help you:

  1. Guide the user in the right direction by telling what exactly they did wrong.
  2. Reduce frustration and increase the user's satisfaction with your app.
  3. Help developers find problems faster by using error logging.

Now that we know why it matters, let's get to it.


Automatic message generation using Bean Validation API

In previous lectures we already added validation annotations. Here's a quick reminder:


import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class UserDto {

    @NotNull(message = "Username can't be empty")
    @Size(min = 2, max = 30, message = "Username must be between 2 and 30 characters")
    private String username;

    @NotNull(message = "Email is required")
    @Size(max = 50, message = "Email cannot exceed 50 characters")
    private String email;

    // Getters and setters
}

Here message provides the string that will be shown to the user when the rule is violated. It's a great start, but we can do even better.


Localizing messages using MessageSource

If you're building an app for multiple languages (for example, Russian and English), it's neat to support localization. Here's how to do it.

Setting up MessageSource

Add a config class for localization:


import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

@Configuration
public class MessageConfig {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource =
                new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages/validation");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}

This MessageSource will read localization files. For example, create validation.properties (by default for English) and validation_ru.properties for Russian:

validation.properties


username.notNull=Username is required
username.size=Username must be between 2 and 30 characters
email.notNull=Email is required
email.size=Email cannot exceed 50 characters

validation_ru.properties


username.notNull=Username can't be empty
username.size=Username must be between 2 and 30 characters
email.notNull=Email is required
email.size=Email cannot exceed 50 characters

Changing the DTO to use localized messages

Now we can use message with keys from the localization files:


import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class UserDto {

    @NotNull(message = "{username.notNull}")
    @Size(min = 2, max = 30, message = "{username.size}")
    private String username;

    @NotNull(message = "{email.notNull}")
    @Size(max = 50, message = "{email.size}")
    private String email;

    // Getters and setters
}

Voila! Now messages are automatically pulled depending on the user's language.


Validation and message construction in controllers

Say we have a controller that handles user creation requests:


import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/users")
@Validated
public class UserController {

    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody UserDto userDto) {
        // Logic to save the user...
        return ResponseEntity.ok("User created successfully!");
    }
}

If a user sends invalid data, for example an empty name, Spring will throw MethodArgumentNotValidException. To handle it and return a nice message, add the following handler:


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

If a user posts:


{
    "username": "",
    "email": "anInvalidEmail@verylongemailthatexceedsfiftycharacters.com"
}

We get a structured, clear JSON back:


{
    "username": "Username can't be empty",
    "email": "Email cannot exceed 50 characters"
}

Localization via HTTP headers

Spring lets you switch languages in requests via the Accept-Language header. For example:


Accept-Language: en

In that case error messages will appear in English. If you specify ru, messages will automatically switch to Russian. This is done via the built-in LocaleResolver.

Add a LocaleResolver:


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;

import java.util.Locale;

@Configuration
public class LocaleConfig {

    @Bean
    public LocaleResolver localeResolver() {
        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
        localeResolver.setDefaultLocale(Locale.ENGLISH);
        return localeResolver;
    }
}

Now the language will be determined automatically based on the request header.


Helpful tips

  1. Write clear messages. Avoid abbreviations and technical jargon. The user should understand what to do.
  2. Log errors. If the user sees only part of the info, log the rest on the server — it'll help find and fix issues.
  3. Test localization. Make sure messages render correctly in all supported languages.

Now you have a powerful tool for showing error messages that make sense to both users and developers. These approaches make an app more professional and friendly. In the next lecture we'll go deeper into fine-tuning message localization and hooking up global error handling.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION