Diagnosing Vaadin “UI Is Not Attached” Failures in Background Tasks
Handle Vaadin background-task crashes after tab close or session timeout by capturing UI, using UI.access(), and enabling @Push correctly.
11 Aug 2026, 06:51 UTC

Recognizable condition
A Vaadin view starts a background task—exporting a report, running a calculation, or polling a remote API—and the task tries to update a label or progress bar after the user closes the browser tab or after the HTTP session expires. The server throws an exception stating that the UI is no longer attached. The background thread may keep running even though the client is gone.
Useful takeaway: Keep the background work, but capture the UI instance before the thread starts, route every component update through UI.access(), and treat a detached UI as an expected exit condition instead of a fatal error.
Cause and diagnostic table
| What you see | Likely cause | Where to look first |
|---|---|---|
| Exception appears immediately after closing the browser tab | UI detached because the client left | Server stack trace around the exception |
| Exception appears after a long idle pause | HTTP session timed out | server.servlet.session.timeout or web.xml session-timeout |
| Exception appears only behind a reverse proxy | WebSocket upgrade blocked by proxy | Browser Network tab; proxy Upgrade and Connection headers |
Exception comes from new Thread() or @Async | UI instance not captured or not wrapped in UI.access() | Code that starts the background task |
Ordered checks
- Confirm push is enabled. Background updates require the
@Pushannotation so the server can send changes without a user request. - Confirm the UI instance is captured before the thread starts. Call
UI.getCurrent()on the request thread and store it in a final variable for the background thread. - Confirm every component mutation runs inside
ui.access(). Vaadin's thread-safety model requires the session lock for UI changes from any non-UI thread. - Confirm session lifetime covers the task, or handle expiry gracefully. If the task can outlive the session, the update handler must catch the detachment exception.
- If behind Nginx, Apache, or a cloud load balancer, confirm WebSocket headers pass through.
@Pushin automatic mode negotiates a WebSocket upgrade; proxies must forwardUpgradeandConnectionheaders.
Fixes tied to findings
1. Enable server-initiated updates with @Push
Add @Push to your application shell or root UI class. Example:
@Push
public class ApplicationConfiguration implements AppShellConfigurator {
// existing configuration
}
Run grep -R "@Push" src/main/java from the project root. Required permissions: read access to the source tree. Expected check: at least one match on the application shell or root view class. Risk: none; this is a read-only search.
2. Capture the UI and marshal updates through UI.access()
UI.access() is Vaadin's mechanism for running code on the UI thread while holding the session lock. ExecutorService is the Java API for managing background threads without creating raw Thread objects.
UI ui = UI.getCurrent();
executor.submit(() -> {
try {
// Simulated long-running work
Thread.sleep(5000);
ui.access(() -> {
statusLabel.setText("Completed");
});
} catch (IllegalStateException e) {
// UI detached: the tab was closed or the session expired
logger.debug("Skipping update: UI is no longer attached", e);
}
});
Do not call access() inside a tight loop; aggregate changes and call it once to reduce locking overhead. Only catch the detachment exception here; let other exceptions propagate so real bugs are not hidden.
3. Adjust session timeout or shorten the task
If background work regularly exceeds the session timeout, raise server.servlet.session.timeout in application.properties or the equivalent web.xml setting. Prefer catching the detachment exception over making sessions arbitrarily long, because long-lived sessions consume server memory.
4. Configure the reverse proxy for WebSocket upgrades
For an Nginx front end, add header forwarding in the location block:
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://vaadin_backend;
}
Without these headers, @Push may fall back to long polling or fail entirely, producing detached-UI errors when the server tries to push an update.
Escalation criteria
- Updates still fail after
@Pushandui.access(): suspect a clustered session, a customUIProvider, or a Vaadin version earlier than Flow 14. - Server logs show rejected threads or out-of-memory errors: the background
ExecutorServicemay be unbounded; switch to a fixed thread pool. - The exception occurs on ordinary button clicks, not background tasks: the problem is likely a stale client-side UI identifier, not the background-task pattern described here.
Limitations and verification
This guide assumes Vaadin Flow (Vaadin 14 or later) and Java 11 or later. Earlier Vaadin versions use a different push and threading model. It does not cover Spring @Async context propagation or session replication across cluster nodes.
Practical verification: start the application, launch a task that runs longer than the session timeout, close the browser tab before completion, and confirm the server log contains no stack trace from the background thread. If the log is clean and the client shows no error popup, the fix is working.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.