Documentation versions (currently viewingVaadin 24)

User Experience Tweaks

Learn how to add keyboard shortcuts and handle errors.

The chat application you’ve been building in this tutorial is almost finished. Before moving on to packaging and deployment, you’ll have to do some small tweaks to improve the user experience.

Add Keyboard Shortcut

The user interfaces of the lobby and the channel view are quite similar. They both have a list at the top, as well as a text field and a button at the bottom. However, in the channel view, you can press Enter instead of clicking the Send button. The send button is also blue in the channel view, whereas it’s gray in the lobby view. You should change the lobby view so that it looks and behaves in the same way as the channel view.

This is very easy to do. Open LobbyView, look for the code in the constructor that initializes addChannelButton and change it like this:

addChannelButton = new Button("Add channel", event -> addChannel());
addChannelButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); // (1)
addChannelButton.addClickShortcut(Key.ENTER); // (2)
  1. This applies the primary theme variant to the button, making it blue.

  2. This associates the Enter key with the button.

You can find more information about keyboard shortcuts in the UI Shortcuts page of the Flow documentation.

Handling Errors

So far, the only errors you’ve handled in the application has been errors that are expected to occur, such as trying to navigate to a channel that doesn’t exist. However, other errors can occur.

It’s a good practice to assume that any call to the application layer — in this case ChatService — can fail. When this happens, an exception is thrown and execution of that method is immediately aborted. The user will experience this error in different ways, depending on where it happens. In some cases, it will look like nothing happened. In other cases, the entire user interface may start to behave strangely. In the worst case, the user will get a 500 internal server error.

You could wrap all the application layer calls with try…​catch, but that would clutter the code. A better way is to handle only expected exceptions in the code (e.g., validation errors or optimistic locking errors), and to let Vaadin’s error handle take care of the unexpected exceptions.

To create your own error handler, you have to do two things: implement the ErrorHandler interface; and register the new error handler with every VaadinSession.

You can find more information about custom error handling in the Custom Error Handler page of the Flow documentation.

Start by creating a new class called, CustomErrorHandler inside the com.example.application.views package like this:

package com.example.application.views;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.server.ErrorEvent;
import com.vaadin.flow.server.ErrorHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Optional;

class CustomErrorHandler implements ErrorHandler {

    private static final Logger log = LoggerFactory.getLogger(CustomErrorHandler.class);

    public void error(ErrorEvent event) {
        log.error("Unexpected error caught", event.getThrowable()); // (1)
        showError("An unexpected error has occurred. Please try again later."); // (2)

    private void showError(String error) {
        Optional.ofNullable(UI.getCurrent()).ifPresent(ui -> ui.access(() -> { // (3)
            var notification =;
  1. To make debugging easier, it’s a good idea to log every unexpected exception.

  2. Don’t reveal any technical information about the error, as this could be used by hackers.

  3. The error notification will only be shown if the current thread has access to a UI. Otherwise, Flow has no idea where to render the notification.

You can find more information about showing notifications in the components documentation.

Next, you have to register the error handler with every new VaadinSession. Vaadin sessions are created by the VaadinService, which is initialized by Vaadin. However, there’s a mechanism that allows developers to customize the service after it has been initialized.

Any Spring beans that implement the VaadinServiceInitListener (from the com.vaadin.flow.server package) will receive an event when the Vaadin service has been initialized. This event contains a reference to the Vaadin service itself, which can be used to register a new SessionInitListener.

The SessionInitListener will receive an event every time a new Vaadin session is created. This is where you should plug in your custom error handler.

This sounds more complicated than it is to implement. The implementation consists of the following steps:

  • Create a new Spring @Configuration class.

  • Inside this class, declare a new bean that implements VaadinServiceInitListener.

  • Use lambas to implement both VaadinServiceInitListener and SessionInitListener.

Create a new class called CustomErrorHandlerConfig inside the com.example.application.views package like this:

package com.example.application.views;

import com.vaadin.flow.server.VaadinServiceInitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

class CustomErrorHandlerConfig {

    public VaadinServiceInitListener vaadinServiceInitListener() {
        return event -> event.getSource().addSessionInitListener( // (1)
            e -> e.getSession().setErrorHandler(new CustomErrorHandler()) // (2)
  1. This is the first lambda that implements VaadinServiceInitListener.

  2. This is the second lambda that implements SessionInitListener and registers the error handler. In this case, since the error handler is stateless, you could turn it into a singleton if you wanted.

You can find more information about the service init listener in the Service Init Listener page of the Flow documentation.

Try It!

To test the error handling, install a tripwire in the application that you can use to trigger unhandled exceptions. In ChatService, add the following lines to the top of the postMessage() method:

if (message.equals("fail")) {
    throw new RuntimeException("I failed!");

You’re now ready to try the new features. Open your browser at http://localhost:8080/ (start the application if it is not already running) and login as admin.

Enter a new channel name at the bottom of the screen and press Enter — instead of the Add channel button. A new channel should be created.

Open the new channel. Enter "fail" in the message field and click Send to submit it. A red error message should be displayed on the screen, and a stacktrace should appear in the console output.

When you’re finished trying the application, remove the tripwire.