How to Solve JSF ViewExpiredException: View Could Not Be Restored
One of the most frustrating errors faced by Java Web developers working with JSF (JavaServer Faces / Jakarta Faces) on application servers like WildFly or JBoss EAP is javax.faces.application.ViewExpiredException: View /index.xhtml could not be restored. This exception occurs during a postback request when JSF fails to restore the state of a view. In this practical guide, we will explore why this happens and implement step-by-step solutions to fix it cleanly in your production applications.
1. Understanding the Root Cause
JSF is a component-based, stateful web framework. When a user requests a page, JSF builds a component tree in memory (the "View"). When the user clicks a button or submits a form (a postback), JSF attempts to restore that exact component tree to process validation, update model values, and invoke action listeners.
The ViewExpiredException happens when JSF receives a postback, but the corresponding component state no longer exists in memory. Common reasons include:
- HTTP Session Expiration: The user was idle past the session timeout limit before submitting a form.
- Server Restart / Redeployment: The application server restarted, clearing all server-side session states.
- Server State Saving Limit Exceeded: JSF limits the number of logical views stored per HTTP session (default is often 15-20 views). Opening many browser tabs overwrites older view states.
- Clustered Failover Without Session Replication: Requests hit a different server node in a WildFly cluster where the session state was not replicated.
2. Solution 1: Adjust State Saving Method (Server vs. Client)
By default, JSF saves view state on the server (inside the HTTP Session). You can change the state-saving strategy in your web.xml descriptor.
Option A: Client-Side State Saving
If you save the state on the client, JSF serializes the view component tree into a hidden HTML input field (javax.faces.ViewState). Because state is stored on the client browser, a session timeout or server restart will never cause a ViewExpiredException.
Add or update this parameter in WEB-INF/web.xml:
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
Client vs Server Tradeoff: Client-side state saving eliminates ViewExpiredException completely, but increases response payload size and network bandwidth due to serialized ViewState sent over the wire.
Option B: Increase Server-Side View Capacity
If you prefer server-side state saving for security and performance, increase the maximum number of logical view states saved per session in web.xml:
<!-- For Mojarra (Standard JSF implementation) -->
<context-param>
<param-name>com.sun.faces.numberOfViewsInSession</param-name>
<param-value>30</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.numberOfLogicalViews</param-name>
<param-value>30</param-value>
</context-param>
3. Solution 2: Use Stateless Views for Static / Login Pages
Starting with JSF 2.2 / Jakarta Faces, you can mark specific views as stateless using transient="true". Stateless views do not store any state in the session, making them completely immune to ViewExpiredException.
Add transient="true" to the <f:view> tag on login forms or static pages:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<f:view transient="true">
<h:head>
<title>Login Page</title>
</h:head>
<h:body>
<h:form>
<h:inputText value="#{loginBean.username}" />
<h:inputSecret value="#{loginBean.password}" />
<h:commandButton value="Login" action="#{loginBean.login}" />
</h:form>
</h:body>
</f:view>
</html>
4. Solution 3: Catch Exception & Redirect Gracefully
Instead of displaying an ugly 500 error stack trace when a session expires, you should gracefully catch ViewExpiredException and redirect the user back to the login or home page.
Using web.xml Exception Handler
Map the exception directly in WEB-INF/web.xml to forward the user to an expired session notification page:
<error-page>
<exception-type>javax.faces.application.ViewExpiredException</exception-type>
<location>/expired.xhtml</location>
</error-page>
Using OmniFaces (Recommended)
If you use the popular OmniFaces utility library, you can automatically restore the view or redirect expired AJAX postbacks using FullAjaxExceptionHandler or EnableRestorableView:
<!-- In faces-config.xml -->
<factory>
<exception-handler-factory>org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory</exception-handler-factory>
</factory>
5. Solution 4: Configure Session Timeout and WildFly Clustering
Increase Session Timeout in web.xml
Ensure your HTTP session timeout is set appropriately for your business workflow (e.g., 30 minutes):
<session-config>
<session-timeout>30</session-timeout>
</session-config>
Enable Distributable Sessions on WildFly / JBoss EAP
If running WildFly in domain/clustered mode behind a load balancer, make sure your application marks its session as distributable in WEB-INF/web.xml so view states replicate across nodes:
<web-app ...>
<distributable/>
</web-app>
6. Troubleshooting & Solution Matrix
| Scenario / Symptom | Probable Cause | Recommended Fix |
|---|---|---|
| Exception occurs after leaving the page open for 30+ minutes. | HTTP Session timeout. | Catch exception in web.xml and redirect to an expired.xhtml page. |
| Exception happens when opening multiple browser tabs. | Exceeded numberOfViewsInSession limit. |
Increase com.sun.faces.numberOfLogicalViews in web.xml. |
| Exception occurs immediately after server deployment/restart. | Server-side state lost during restart. | Use transient="true" on login pages or switch to client state saving. |
| Exception during AJAX requests. | AJAX postback sent to expired view state. | Use OmniFaces FullAjaxExceptionHandler to handle AJAX redirects cleanly. |
7. Frequently Asked Questions (FAQ)
What is the difference between ViewExpiredException in JSF 2 and Jakarta Faces 4?
In Jakarta EE 10+ (Jakarta Faces 4.0), the package name migrated from javax.faces.application.ViewExpiredException to jakarta.faces.application.ViewExpiredException. The resolution logic remains identical.
Is client-side state saving safe for security-sensitive applications?
Yes, provided encryption and signing are enabled (which JSF implementations like Mojarra and MyFaces do automatically). However, client state saving increases payload bandwidth.
Can I auto-refresh the page when the session expires?
Yes, you can use a simple JavaScript meta refresh tag or JSF poll component tuned to trigger slightly before the HTTP session timeout duration.
Conclusion
Handling ViewExpiredException is a mandatory step when building robust, production-grade JSF and Jakarta Faces applications. By choosing between client/server state saving, marking static forms as stateless (transient="true"), and setting up a proper exception redirect handler in web.xml, you can deliver a seamless user experience on WildFly and JBoss servers.
Recommended Articles
JSF Data Validation Tutorial: Enhance Your Applications with JSF 2.0
Learn how to leverage JSF validation for cleaner, more efficient applications. #Java #Middleware #CloudNative
Create a Custom JSF Converter for User Object - Jakarta Server Faces (JSF) Tutorial
Learn how to create a custom JSF converter for User objects in Java applications using WildFly 31 and Java 21. #Java #Middleware #CloudNative
Comparing Object Storage Options for Web Applications with JSF 2 and CDI
Explore JSF 2 scopes and CDI extensions for storing object data in web applications. #JSF #CDI #WebDevelopment
Create JSF Custom Tags Using XHTML in Enterprise Java Applications
Learn how to create reusable forms and settings using custom JSF tags based on an XHTML page. #EnterpriseJava #JSF #CloudNative