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