PrimeFaces provides a powerful ExceptionHandler out of the box with following features:
<div class="card">
    <h:form>
        <h5 class="mt-0">AJAX</h5>
        <p:commandButton action="#{exceptionHandlerView.throwViewExpiredException}"
                         ajax="true"
                         value="Throw ViewExpiredException!" styleClass="mr-2" />
        <p:commandButton action="#{exceptionHandlerView.throwNullPointerException}"
                         ajax="true"
                         value="Throw NullPointerException!"/>
        <!-- IllegalStateException is not handled using ajaxExceptionHandlers below, so the error page is shown-->
        <p:commandButton action="#{exceptionHandlerView.throwWrappedIllegalStateException}"
                         ajax="true"
                         value="Throw IllegalStateException!" styleClass="mr-2"/>
        <h5>Non-AJAX</h5>
        <p:commandButton action="#{exceptionHandlerView.throwViewExpiredException}"
                         ajax="false"
                         value="Throw ViewExpiredException!" styleClass="mr-2"/>
        <!-- NullPointerException has no specific error-page defined in web.xml compared to ViewExpiredException -->
        <!-- https://github.com/primefaces/primefaces/blob/master/primefaces-showcase/src/main/webapp/WEB-INF/web.xml -->
        <p:commandButton action="#{exceptionHandlerView.throwNullPointerException}"
                         ajax="false"
                         value="Throw NullPointerException!"/>
        <p:ajaxExceptionHandler type="jakarta.faces.application.ViewExpiredException"
                                update="exceptionDialog"
                                onexception="PF('exceptionDialog').show()" />
        <p:ajaxExceptionHandler type="java.lang.NullPointerException"
                                update="exceptionDialog"
                                onexception="PF('exceptionDialog').show()"/>
        <p:dialog id="exceptionDialog" header="Exception '#{pfExceptionHandler.type}' occured!"
                  widgetVar="exceptionDialog"
                  height="500px">
            Message: #{pfExceptionHandler.message} <br/>
            StackTrace: <h:outputText value="#{pfExceptionHandler.formattedStackTrace}" escape="false"/> <br/>
            <p:button onclick="document.location.href = document.location.href;"
                      value="Reload!"
                      rendered="#{pfExceptionHandler.type == 'jakarta.faces.application.ViewExpiredException'}"/>
        </p:dialog>
    </h:form>
</div>
package org.primefaces.showcase.view.misc;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.FacesException;
import jakarta.faces.application.ViewExpiredException;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
@Named
@RequestScoped
public class ExceptionHandlerView {
    public void throwNullPointerException() {
        throw new NullPointerException("A NullPointerException!");
    }
    public void throwWrappedIllegalStateException() {
        Throwable t = new IllegalStateException("A wrapped IllegalStateException!");
        throw new FacesException(t);
    }
    public void throwViewExpiredException() {
        throw new ViewExpiredException("A ViewExpiredException!",
                FacesContext.getCurrentInstance().getViewRoot().getViewId());
    }
}