Error Handling

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Pages 4.0 (section 1.4 "Error Handling" and the ErrorData Javadoc), Jakarta Expression Language 6.0, Jakarta Standard Tag Library 3.0 and Jakarta Servlet 6.1 (section 10.9 "Error Handling" and the deployment descriptor), running on Apache Tomcat 11.0.26 with Java 21. Everything was verified on 2026-10-08: the facts against the specifications, their Javadoc and the Tomcat 11.0 documentation, and every example by building it into a Maven WAR, deploying it as g3errors.war on Tomcat 11.0.26 and calling it with curl; the outputs shown are the outputs that run produced (headers that do not matter, such as Set-Cookie and Date, and blank lines are left out). A page that fails is a fact of life: a database goes away, a parameter is not a number, a book is withdrawn. What matters is what the visitor sees, what the operator finds in the log, and whether the failure leaks anything an attacker can use. The running scenario is the Bookshelf, a small catalogue: its detail page /book?id=... is forwarded to a JSP, and the page grows from "print a stack trace" to an error page that shows a reference number, logs the details once and escapes everything it prints.

Where an Error Can Happen

Section 1.4 of the Pages specification separates two moments, and a third one appears in practice. The table lists them with what each produces and what the specification promises.

Table 1. The three moments at which a JSP fails
Moment Examples What the container does Where the specification speaks

Translation time

Unterminated tag, unknown directive attribute, exception used in a page that is not an error page, missing taglib

A fatal translation failure makes every later request for that page fail; for HTTP the status is 500. If translation happens before the first request, notification is implementation dependent

1.4.1

Request time, before the buffer is flushed

An uncaught Throwable from EL, a tag handler, a scriptlet or a called class

The request and the exception are forwarded to the errorPage URL of the page, or to the implementation default when there is none; the error is passed as request attributes

1.4.2

Request time, after the buffer was flushed

The same exceptions, but the response is already committed

A dispatch to an error page "may fail"; the status line has been sent and cannot change

buffer attribute, 1.10.1

The two mechanisms that can answer a failure are the JSP page directive (errorPage, per page) and the deployment descriptor (<error-page>, per application, by status code or exception type). The diagram shows how Tomcat 11 chooses between them for a request that reaches a JSP. The text of the specification is: if the page defines an error page through the page directive, the error pages of web.xml are not used for that page.

flowchart TD req["Request reaches a JSP or a servlet"] --> fail{"Translation error, or uncaught Throwable, or sendError?"} fail -->|"no"| ok["Normal response"] fail -->|"translation error"| web{"web.xml error-page matches?"} fail -->|"uncaught Throwable"| dir{"Page directive has errorPage and the buffer is not committed?"} fail -->|"sendError(status)"| web dir -->|"yes"| jsp["Forward to the errorPage JSP: exception, errorData, request attributes"] dir -->|"no"| web web -->|"error-code or exception-type or default"| page["ERROR dispatch to the location"] web -->|"no match"| valve["Tomcat ErrorReportValve writes its own report"]

The branch "translation error" goes to web.xml because the errorPage attribute of a page that did not compile does not exist at run time; the example in Translation-Time Errors shows it.

The Bookshelf Used on This Page

All the examples are files of one WAR, deployed with the context path /g3errors. The Java classes are in the package org.example.bookshelf (imports and the package line are left out of the listings). The JSPs live under WEB-INF/views, so they are reachable only through a servlet forward, and the error pages under WEB-INF/views/errors. The pom.xml is the one from the Getting Started page of this section (Jakarta Servlet 6.1, Pages 4.0 and EL 6.0 with scope provided, JSTL 3.0 in the WAR).

Table 2. The files of the example WAR
File Mapping or role Concept

PageServlet

/book, /book-plain, /safe, /buffered, /self, /broken, /legacy, /noerr

forwards to WEB-INF/views/<name>.jsp

book.jsp

/book

page with errorPage, EL method call that can throw

book-plain.jsp

/book-plain

the same page without errorPage, so web.xml decides

error.jsp

target of errorPage

isErrorPage, ErrorData, logging, reference id

errors/not-found.jsp, errors/book-missing.jsp, errors/generic.jsp

web.xml <error-page> locations

status-code, exception-type and Throwable pages

safe.jsp

/safe

c:catch

buffered.jsp, self.jsp, broken.jsp, noerr.jsp

/buffered, /self, /broken, /noerr

committed response, self-referencing error page, translation errors

legacy.jsp, legacy-error.jsp

/legacy

the scriptlet "before" snippet

StatusServlet

/withdrawn, /teapot, /boom, /missing

sendError(410), sendError(418), IllegalStateException, BookNotFoundException

Catalog, Book, BookNotFoundException, ErrorLog, AppListener

application scope

the model, the exception, one-time logging

The model is a catalogue whose find method throws a runtime exception for an unknown id, and a listener that puts the catalogue and the logger into application scope.

Catalog.java and BookNotFoundException.java: the failure the pages will trigger
public class BookNotFoundException extends RuntimeException {
    public BookNotFoundException(long id) {
        super("No book with id " + id);
    }
}

public class Catalog {
    private final List<Book> books = List.of(
            new Book(1, "Dune", "Frank Herbert", 1965),
            new Book(2, "Neuromancer", "William Gibson", 1984));

    public Book find(long id) {
        return books.stream().filter(b -> b.id() == id).findFirst()
                .orElseThrow(() -> new BookNotFoundException(id));
    }
}

errorPage and isErrorPage

Two attributes of the page directive connect a failing page to the page that reports the failure.

Table 3. The page directive attributes that concern errors (Table JSP.1-8)
Attribute Meaning Default and rules

errorPage

URL of the resource that receives any Throwable thrown but not caught by the page. The URL is relative: starting with / it is relative to the web application, otherwise to the current page

The default is implementation dependent. If the URL names a JSP, that page’s exception variable holds the original Throwable. The page directive wins over web.xml: error pages from the descriptor are not used for a page that defines one

isErrorPage

Marks the page as a possible target of another page’s errorPage

false. If true, the implicit variable exception is defined; if false, referring to exception is a fatal translation error

buffer

Response buffer size; interacts with errors because only an uncommitted response can be redirected to an error page

8kb or more, see below

The failing page and the reporting page of the Bookshelf are two files. The first calls a method of the catalogue from EL, so an unknown id throws.

book.jsp: a page that names its error page
<%@ page errorPage="/WEB-INF/views/error.jsp" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="book" value="${catalog.find(param.id)}"/>
<!DOCTYPE html>
<html>
<body>
<h1><c:out value="${book.title}"/></h1>
<p>by <c:out value="${book.author}"/> (${book.year})</p>
</body>
</html>

A normal request renders the book; an unknown id sends the request and the exception to error.jsp, which the next sections write. The status of the response is 500, as the specification prescribes for an error page:

curl -si 'http://localhost:18080/g3errors/book?id=1'     # 200, <h1>Dune</h1>
curl -si 'http://localhost:18080/g3errors/book?id=99'    # 500, the error page below

The isErrorPage rule is enforced when the page is translated. A page that is not an error page and prints exception does not compile; Tomcat reports exception cannot be resolved to a variable and the visitor gets the error page of web.xml.

noerr.jsp: exception without isErrorPage (translation error, observed)
<%@ page contentType="text/html; charset=UTF-8" %>
<html><body><%= exception %></body></html>

Getting at the Exception: exception, ErrorData and Request Attributes

An error page has four ways to see what went wrong. They are not equivalent, and one of them, the exception variable, is not available in EL at all.

Table 4. The ways an error page can read the failure
Expression Type Notes

<%= exception %> (scripting variable)

Throwable

Defined only when isErrorPage="true". Set from the request attribute jakarta.servlet.error.exception, otherwise from jakarta.servlet.jsp.jspException (kept for pages precompiled with a JSP 1.2 compiler). Scriptlets only

${pageContext.exception}

Throwable

PageContext.getException(); works in EL. This is the EL way to get the exception

${pageContext.errorData.throwable}

Throwable

The ErrorData view of the same exception; null when the error came from sendError instead of an exception

${requestScope['jakarta.servlet.error.exception']}

Throwable

The raw request attribute from the Servlet specification. jakarta.servlet.error.exception_type, .message, .status_code, .request_uri, .servlet_name, .query_string and .method sit next to it

${exception} in EL is empty even on a page with isErrorPage="true": the table 1-7 variable is a scripting variable, and the JSP expression-language resolvers do not know a name exception. On Tomcat 11.0.26 the expression printed nothing, while ${pageContext.exception} printed the exception class. Use pageContext.

The class of the exception is not always the class you threw. When an EL method call throws, Jasper wraps the exception in a org.apache.jasper.el.JspELException whose cause is yours, and a servlet that forwards to the JSP sees that exception rethrown as a jakarta.servlet.ServletException. The error page below prints the chain so that the layers are visible:

The book.jsp?id=99 chain, as printed by error.jsp (observed)
throwable: org.apache.jasper.el.JspELException
pageContext.exception: org.apache.jasper.el.JspELException
cause: org.example.bookshelf.BookNotFoundException
root cause:

A scriptlet that calls catalog.find directly throws BookNotFoundException itself, with no wrapper. Code that wants the original exception walks getCause() until it reaches the application’s own type.

Writing ${exception.class.name} to print the class does not parse on Tomcat 11.0.26: the page fails with a JasperException "Failed to parse the expression". Use the bracket form ${exception['class'].name} or call ${exception.getClass().getName()}; both printed the name in the test. This matters because many error-page examples written for older containers use the dot form.

ErrorData

jakarta.servlet.jsp.ErrorData is an immutable bean built from the Servlet error attributes and exposed as pageContext.errorData (PageContext.getErrorData()). It is meaningful only on an error page. Because the properties are plain getters, EL reads them directly, which the specification gives as the reason for the class: ${pageContext.errorData.statusCode}.

Table 5. ErrorData properties (Jakarta Pages 4.0 Javadoc)
EL property Java method Value Since and notes

statusCode

int getStatusCode()

The status code of the error; 500 for an uncaught exception

JSP 2.0

requestURI

String getRequestURI()

The request URI. On book.jsp?id=99 it was /g3errors/WEB-INF/views/book.jsp, the forward target and not the URL the visitor typed (/book); when web.xml handled a servlet error it was /g3errors/book-plain

JSP 2.0

servletName

String getServletName()

The name of the servlet invoked: jsp for the JSP servlet, the class name for StatusServlet, pages for PageServlet

JSP 2.0

throwable

Throwable getThrowable()

The Throwable that caused the error; null when sendError was used

JSP 2.0

method

String getMethod()

The request method; printed GET and, for a POST, POST

JSP 4.0: new

queryString

String getQueryString()

The raw query string, or null when there was none; the value printed for ?id=99 was id=99 and keeps percent-encoding

JSP 4.0: new

The constructor ErrorData(Throwable, int, String, String) is deprecated for removal since 4.0 and replaced by ErrorData(Throwable, int, String, String, String, String) with the method and the query string. Only containers construct ErrorData; application code never calls it.

Writing an Error Page: Reference, Log, Escape, Do Not Leak

An error page is the one page an attacker can trigger on demand, so it follows four rules: show the visitor a short message and a reference number, log the details once on the server, escape everything that came from the request, and never print a stack trace or an exception message. The Bookshelf puts the logging in a small application-scoped object so that it is shared by every error page and testable.

ErrorLog.java: log once, return a reference the visitor can quote
public class ErrorLog {
    private static final Logger LOG = Logger.getLogger(ErrorLog.class.getName());

    public String report(ErrorData data) {
        String ref = UUID.randomUUID().toString().substring(0, 8);
        String line = "error ref=%s status=%d method=%s uri=%s query=%s servlet=%s".formatted(
                ref, data.getStatusCode(), data.getMethod(), data.getRequestURI(),
                data.getQueryString(), data.getServletName());
        if (data.getThrowable() != null) {
            LOG.log(Level.SEVERE, line, data.getThrowable());
        } else {
            LOG.log(Level.WARNING, line);
        }
        return ref;
    }
}

The error page calls it from EL, prints only escaped values, and keeps the diagnostic list short so that the example shows each ErrorData property; a production page shows only the heading and the reference.

error.jsp: the target of errorPage
<%@ page isErrorPage="true" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="ref" value="${errorLog.report(pageContext.errorData)}"/>
<!DOCTYPE html>
<html>
<body>
<h1>Something went wrong (<c:out value="${pageContext.errorData.statusCode}"/>)</h1>
<p>Quote reference <strong><c:out value="${ref}"/></strong> when you contact support.</p>
<ul>
  <li>method: <c:out value="${pageContext.errorData.method}"/></li>
  <li>uri: <c:out value="${pageContext.errorData.requestURI}"/></li>
  <li>query: <c:out value="${pageContext.errorData.queryString}"/></li>
  <li>servlet: <c:out value="${pageContext.errorData.servletName}"/></li>
  <li>throwable: <c:out value="${pageContext.errorData.throwable['class'].name}"/></li>
  <li>pageContext.exception: <c:out value="${pageContext.exception['class'].name}"/></li>
</ul>
</body>
</html>

The visible result for a missing book, and the single log line that accompanied it:

$ curl -si 'http://localhost:18080/g3errors/book?id=99'
HTTP/1.1 500
Content-Type: text/html;charset=UTF-8

<h1>Something went wrong (500)</h1>
<p>Quote reference <strong>b009b2f8</strong> when you contact support.</p>
  <li>method: GET</li>
  <li>uri: /g3errors/WEB-INF/views/book.jsp</li>
  <li>query: id=99</li>
  <li>servlet: jsp</li>
  <li>throwable: org.apache.jasper.el.JspELException</li>

# Tomcat log, one entry with the stack trace under it:
SEVERE [http-nio-18080-exec-280] org.example.bookshelf.ErrorLog.report error ref=b009b2f8 status=500 method=GET uri=/g3errors/WEB-INF/views/book.jsp query=id=99 servlet=jsp

A request such as ?id=%3Cscript%3Ealert(1)%3C/script%3E is a hostile value. getQueryString() returns the raw string and the error page printed it through c:out, so the page contained id=%3Cscript%3E... as inert text. Printing ${pageContext.errorData.queryString} without c:out, or building a link from it, would be a reflected cross-site scripting hole; the same holds for requestURI, which also comes from the request.

The status of a JSP error page is, by default, ${pageContext.errorData.statusCode}; the specification allows the page to set another value, including 200. Do not: a 200 turns an error into a "soft error" that crawlers index and monitoring does not count.

Because the directive path is a forward (not an ERROR dispatch), a filter mapped to <dispatcher>ERROR</dispatcher> does not run for pages that use errorPage, so a filter is the wrong place to log them. A bean called from the error page logs both paths, as long as every error page calls it.

Error Pages in web.xml: Status Codes, Exception Types and the Default

When a page has no errorPage attribute, or the failure is not an exception of a JSP (sendError, a servlet exception, a 404), the deployment descriptor decides. The container matches the failure to an <error-page>; the page given by <location> receives an ERROR dispatch. The Servlet 6.1 rules, summarised:

Table 6. How the container picks an <error-page> (Servlet 6.1, section 10.9.2)
Failure Matching rule

response.sendError(code)

An <error-code> entry for that status; else the default entry

An exception or error that escapes

An <exception-type> entry; the closest match in the class hierarchy wins. If none fits and the exception is a ServletException, a second pass uses getRootCause()

Neither fits

The default entry, an <error-page> with neither <error-code> nor <exception-type>

Several entries

Entries must be unique by status code and by exception class name

Dispatch from code

The mechanism does not intervene when errors occur through RequestDispatcher or doFilter, so the caller may handle them

The Bookshelf declares three entries:

web.xml: error pages by status code and exception type
<error-page>
  <error-code>404</error-code>
  <location>/WEB-INF/views/errors/not-found.jsp</location>
</error-page>
<error-page>
  <exception-type>org.example.bookshelf.BookNotFoundException</exception-type>
  <location>/WEB-INF/views/errors/book-missing.jsp</location>
</error-page>
<error-page>
  <exception-type>java.lang.Throwable</exception-type>
  <location>/WEB-INF/views/errors/generic.jsp</location>
</error-page>

The locations may be under WEB-INF, which keeps the error pages out of reach of a direct request. The three entries were exercised with curl; the line that explains each result follows the output.

$ curl -s http://localhost:18080/g3errors/nothing          # HTTP 404, the status-code page
<h1>Not found</h1>
<p>Nothing lives at <code>/g3errors/nothing</code>.</p>
<p>status=404 throwable= exception=</p>

$ curl -s http://localhost:18080/g3errors/missing          # HTTP 500, matched by exception type
<h1>No such book</h1>
<p>No book with id 99</p>
<p>matched by type, exception=org.example.bookshelf.BookNotFoundException, status=500</p>

$ curl -s 'http://localhost:18080/g3errors/book-plain?id=99'   # HTTP 500, no errorPage: the Throwable entry
<h1>Error 500</h1>
<p>Reference 64f5ca11. Throwable: jakarta.servlet.ServletException,
caused by org.example.bookshelf.BookNotFoundException,

The third result is a pitfall. /book-plain throws the same BookNotFoundException, but the exception reaches the container as a ServletException that wraps the JSP failure. The first pass matches the java.lang.Throwable entry (every exception is a Throwable), so the second, root-cause pass never happens and the specific BookNotFoundException page is not used. Either map the wrapper types the JSP engine produces, give the JSP an errorPage, unwrap in a filter, or let the generic page branch on the cause. A servlet that throws the exception itself (/missing) matches the specific entry, as the second result shows.

An error page reached from web.xml reads the same information. pageContext.errorData works on those pages too, and the request attributes of the Servlet 6.1 error dispatch are visible; the page printed status=500, method=GET, uri=/g3errors/book-plain, servlet=pages and type=class jakarta.servlet.ServletException for the third request. A POST to /book?id=99 printed method: POST, because the request that failed was a POST while the error dispatch itself is always a GET.

The error attributes, the ERROR dispatcher type and the rule that an error page is invoked through a GET are explained with their own listings on the error pages section of Servlet Fundamentals; this page adds only what is specific to JSP.

Table 7. Page directive or web.xml? (observed on Tomcat 11.0.26)
Situation Handled by Reason

Uncaught exception, page has errorPage

The page’s errorPage

The directive wins; the Throwable entry in web.xml was not used for book.jsp

Uncaught exception, page has no errorPage

web.xml by exception type

/book-plain

sendError(404) or a missing URL

web.xml by status code

/nothing

sendError(410) with no <error-code> 410 and no default entry

Tomcat’s own report

/withdrawn prints the Tomcat report with the message and the server version

Translation error

web.xml

/broken and /noerr were answered by the Throwable entry

c:catch: Handling a Failure Inside the Page

c:catch (see JSTL Core) catches any Throwable thrown by the actions nested in it and, optionally, stores it in a page-scoped variable. It is the granular alternative: the specification says actions of central importance should not be wrapped, so that their exception reaches the error page, while actions of secondary importance are wrapped so that they never invoke the error-page mechanism. The variable has page scope, is removed if no exception occurred, and without var the exception is swallowed.

safe.jsp: a secondary box on the page that must not break it
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<body>
<c:catch var="problem">
  <c:set var="book" value="${catalog.find(param.id)}"/>
  <h1><c:out value="${book.title}"/></h1>
</c:catch>
<c:if test="${not empty problem}">
  <p class="warning">That book is not available.</p>
</c:if>
</body>
</html>

With ?id=99 the response is a normal 200 page with the paragraph "That book is not available."; with ?id=1 it prints <h1>Dune</h1>. A first version of the page also put the exception into an HTML comment for the developer; it printed the class name (org.apache.jasper.el.JspELException), the source line of the page and the EL expression to anyone who viewed the page source. Put diagnostics in the log, not in the response. c:catch has two more limits: everything that was already written stays written (the partial output before the failure is not removed unless the buffer is reset), and a swallowed exception is never logged unless the page logs it.

A Committed Response Cannot Be Redirected

The buffer attribute of the page directive sets the response buffer. Output is held until the buffer fills or the page ends; only until then can the container still replace the response with an error page. After the buffer was flushed the status line and the headers are on the wire. The specification says a later attempt to dispatch an uncaught exception to an error page "may fail".

buffered.jsp: 200 rows, a 1 kB buffer, an exception at the end
<%@ page buffer="1kb" errorPage="/WEB-INF/views/error.jsp" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<body>
<c:forEach begin="1" end="200" var="i">
<p>Row ${i} of the catalogue export</p>
</c:forEach>
<c:set var="late" value="${catalog.find(99)}"/>
</body>
</html>

The observed response was HTTP/1.1 200 with Transfer-Encoding: chunked and 7715 bytes: 181 of the 200 rows, flushed in 1 kB chunks, then a second <html> document, the error page, appended after them. The client received a broken page with a success status. The fixes are the default buffer (8 kB or more) for pages with a late failure, computing everything that can fail before the first byte is written (load the data in the servlet, then forward), and never relying on the error page for a page that streams. Setting buffer="none" makes it worse, because every byte is sent at once.

Translation-Time Errors

A JSP that does not compile has no running code, so its errorPage cannot apply. The visitor and the log see a org.apache.jasper.JasperException.

broken.jsp: an unterminated tag (errorPage is declared but cannot take effect)
<%@ page errorPage="/WEB-INF/views/error.jsp" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<html><body>
<c:forEach var="b" items="${catalog.books}">
  <p>${b.title}</p>
</body></html>

On Tomcat 11.0.26 the log says broken.jsp (line: [6], column: [7]) Unterminated [<c:forEach] tag, the status is 500, and the page rendered was the Throwable entry of web.xml (errors/generic.jsp, printing org.apache.jasper.JasperException), not error.jsp. In production, precompile the JSPs at build time so that translation errors show up in the build instead of on the first request; the Jasper configuration page of this section covers precompilation.

Error Pages That Fail or Refer to Themselves

An error page is itself a page and can fail. The specification requires the container to detect a self-referencing error page and throw a translation error. The test below points the errorPage of a page at the page itself and makes the page fail.

self.jsp: an error page whose errorPage is itself
<%@ page errorPage="/WEB-INF/views/self.jsp" isErrorPage="true" contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="x" value="${catalog.find(99)}"/>
<html><body>self</body></html>

Tomcat 11.0.26 did not report a translation error. The page compiled and ran; the failure forwarded to itself again and again until the stack overflowed, and the container answered 500 with a ServletException caused by a java.lang.StackOverflowError, which web.xml rendered through the Throwable page. With no exception thrown, a self-referencing page behaves normally (the first version of the test answered 200). So keep error pages trivial: no calls that can throw, no errorPage of their own, and no dependency on the state that failed (a database, the session).

The Container’s Own Error Report

When no error page matches, Tomcat writes its own HTML report through the ErrorReportValve. For sendError(410, "This book was withdrawn") it printed the status, the type, the message, a description and the server version Apache Tomcat/11.0.26; for an exception it also prints the stack trace unless the valve is configured otherwise. Both pieces are information an attacker uses: the product and version select the exploit, the stack trace shows packages and file names. The valve has two switches (default true) and can also return static files:

Table 8. Error Report Valve attributes (Tomcat 11.0 documentation)
Attribute Meaning Default

showReport

Whether the error report (message and stack trace) is in the HTML

true

showServerInfo

Whether the server version is in the HTML

true

errorCode.nnn

Path of a UTF-8 HTML file to return for status nnn; errorCode.0 is the default file

none

exceptionType.fullyQualifiedClassName

Path of a UTF-8 HTML file for an exception class and its subclasses

none

Disabling both returns only the status code. The valve belongs to the host, normally declared in conf/server.xml:

server.xml: a quiet error report (from the Tomcat documentation, not run)
<Valve className="org.apache.catalina.valves.ErrorReportValve" showReport="false" showServerInfo="false"/>

The shared Tomcat of this test could not be reconfigured, so the host-level snippet above was not run. What was run is the temptation to keep the setting in the application: a META-INF/context.xml with the same <Valve> inside <Context>. It changed the report to the bare "HTTP Status 418 – Unknown Reason" as intended, but it also made every <error-page> stop working: the 404 page, the exception pages and the Throwable page were all replaced by the bare status page, because the context-level valve writes the report before the host dispatches to the descriptor’s error pages. Configure the valve at the host, or leave the application’s <error-page> entries to do the work and make sure every status and the Throwable has one, including a default entry.

The Scriptlet Way and Why It Leaks

Older JSP code prints the exception from a scriptlet. This is a "before" snippet kept only to recognise it in legacy code.

legacy-error.jsp: the old pattern (do not copy)
<%@ page isErrorPage="true" import="java.io.PrintWriter" contentType="text/html; charset=UTF-8" %>
<html><body>
<h1>Oops</h1>
<pre><% exception.printStackTrace(new PrintWriter(out)); %></pre>
</body></html>

With /legacy?id=99 the page answered 500 and printed 3204 bytes: the exception message, the application’s class names and line numbers (Catalog.java:16, legacy_jsp.java:123), and the Jasper and Tomcat internals. The replacement is the page above: the reference id for the visitor, ErrorLog for the operator. A scriptlet that reads exception gets the BookNotFoundException itself, with no JspELException wrapper, because the exception was thrown by the scriptlet code and not by EL.

Errors in Struts

Struts 7 does not use errorPage. Action exceptions are handled by exception-mapping elements and the exception interceptor, and validation errors by field and action errors; see Exception Handling and Global Results and Validation in the Struts part of this section. The JSP side of a Struts application is still the same: a JSP result that can fail has the same errorPage, web.xml and escaping rules as above, and the container error pages catch what the framework does not.

Pitfalls and Error Names

Table 9. Error handling pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Tomcat 11.0.26)
Symptom or error Cause Fix

Empty output where ${exception.message} should be (observed)

exception is a scripting variable, not an EL implicit object

${pageContext.exception.message} or pageContext.errorData.throwable

JasperException: "Failed to parse the expression" for ${exception.class.name} (observed)

The dot form .class does not parse in EL on Tomcat 11.0.26

${exception['class'].name} or getClass().getName()

exception cannot be resolved to a variable (observed)

exception used in a page without isErrorPage="true"

Set isErrorPage="true" on the error page only

The exception-type page for my exception never shows up (observed)

EL wraps it in JspELException; a forward wraps it in ServletException; a Throwable entry matches in the first pass

Map the wrapper types, add errorPage, unwrap getCause() in the page

Two documents in one response and a 200 status (observed)

The failure happened after the buffer was flushed

Compute first, forward later; keep the default buffer; do not stream from a JSP

The errorPage of a broken page is ignored (observed)

The page did not compile, so its directive does not exist at run time

Precompile; keep a default <error-page>

Endless forward ending in StackOverflowError (observed)

The error page’s own errorPage is itself, or the error page throws

Keep error pages trivial and without errorPage

Stack trace, class names or Apache Tomcat/11.0.26 in the browser (observed)

Scriptlet printStackTrace, an HTML comment with the message, or the default Tomcat report

Reference id plus log; showReport and showServerInfo off at the host; an <error-page> for every status and a Throwable default

<script> from the query string runs in the error page

requestURI or queryString printed without escaping

c:out or fn:escapeXml on every ErrorData string

All <error-page> entries stop working after adding a valve to context.xml (observed)

A context-level ErrorReportValve writes the report first

Configure the valve at the host

errorData.requestURI shows /WEB-INF/views/book.jsp, not /book (observed)

The error is reported for the forward target

Log the original request from a filter or jakarta.servlet.forward.request_uri

An error filter never sees my JSP failure

The errorPage path is a forward, not an ERROR dispatch

Log from the error page, as ErrorLog does

A 404 or 500 answered with status 200

The error page set the status, or a proxy replaced it

Leave the status the container set

IllegalStateException while writing an error page

The response was already committed

Same cure as the committed-response example

What Changed Recently

  • Jakarta Pages 4.0 (Jakarta EE 11): ErrorData gained getMethod() and getQueryString() and a six-argument constructor, matching the new Servlet error attributes jakarta.servlet.error.method and jakarta.servlet.error.query_string; the four-argument constructor is deprecated for removal (change log items "Update ErrorData to add support for the new attribute…​").

  • Jakarta Servlet 6.1: introduces RequestDispatcher.ERROR_METHOD and ERROR_QUERY_STRING (both "Since: Servlet 6.1" in the Javadoc). The specification’s Table 10-1 does not list jakarta.servlet.error.method; the Javadoc and change log do.

  • Jakarta Pages 3.1 and 4.0: errorOnELNotFound (3.1) turns an unknown EL identifier into a PropertyNotFoundException, which then goes through the error-page mechanism; 4.0 removed the deprecated isThreadSafe attribute, the jsp:plugin action and other items unrelated to errors.

  • EL 6.0: the Tomcat 11.0.26 observations about .class and about wrapped exceptions apply to this stack; re-test when changing containers.

  • Jakarta EE 9 and later: every javax.servlet.error.* attribute is jakarta.servlet.error.*.

Where the Books Are Outdated

The JSP 1.2 and JSTL 1.0 books cover the same ideas, and the ideas still hold: an errorPage attribute, an isErrorPage attribute, a c:catch for the page-local failure, error pages that collect diagnostics, and pageContext.exception for JSTL pages. What is different today:

Table 10. Error handling in the books against Jakarta Pages 4.0
Older material Today

javax.servlet.error.* and javax.servlet.jsp.jspException

jakarta.servlet.error.*; jakarta.servlet.jsp.jspException survives only as a fallback for pages precompiled with a JSP 1.2 compiler

Error page prints exception.printStackTrace or getMessage() for the visitor

Never; show a reference and log the details. Containers add their own report unless it is turned off

exception used as if it were available everywhere

Only when isErrorPage="true", and not from EL

${exception.class.name} and similar dot forms

Failed to parse on Tomcat 11.0.26; use the bracket form

ErrorData with four properties (JSP 2.0)

Six, with method and queryString since Pages 4.0

web.xml DTD examples (Servlet 2.3) with <error-page>

Same elements, but the descriptor uses the Jakarta EE namespace and version="6.1"

Error pages in the public web root

Put them under WEB-INF

Scriptlet error handling, try/catch in pages

c:catch for secondary content; move the logic into the servlet or a service