Exception Handling and Global Results

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 Apache Struts 7.4.0 (released 2026-10-02), running on Apache Tomcat 11.0.26 (Jakarta Servlet 6.1, Jakarta Pages 4.0) with Java 21. Everything was verified on 2026-10-08: the facts against the Struts documentation, the struts-default.xml, default.properties and DTD files inside the 7.4.0 struts2-core jar and the bytecode of org.apache.struts2.interceptor.ExceptionMappingInterceptor, and every example by building it into a Maven WAR with the struts2-bom and deploying it on Tomcat 11.0.26, where each URL was called with curl; the outputs shown are the outputs that run produced (headers that do not matter and blank lines are left out). Action code throws exceptions: a book that does not exist, a database that is down, a bug. What the visitor sees, what the operator finds in the log, and which HTTP status the browser, a proxy or a search engine receives are three separate decisions, and Struts lets the configuration make them without a try block in every action. The running scenario is the Bookshelf catalogue: the page grows from an action that throws and a stack trace in the browser to a configuration in which every exception ends in a polite page, a correct status code and one log entry.

The Idea: An Exception Is Just Another Result Code

An action method returns a result code such as success or input, and the framework looks that code up in the action’s result elements and then in the package’s global-results. Exception mapping adds one rule: when the action throws, the exception is converted into a result code by a declaration, and from then on the request is processed as if the action had returned that code. The Core Developers Guide calls this "a powerful feature for dealing with an Action class that throws an Exception" and says it is especially useful for frameworks that throw runtime exceptions.

Three pieces cooperate:

  1. exception-mapping elements say "an exception of this class (or a subclass) means this result code".

  2. result and global-results elements say what that code does: render a JSP, redirect, chain to another action, or send an HTTP error.

  3. The exception interceptor, an instance of org.apache.struts2.interceptor.ExceptionMappingInterceptor, catches the exception, looks up the mapping and pushes the exception onto the value stack. Without that interceptor in the action’s stack the mappings are ignored, a point on which the interceptor’s documentation is explicit.

flowchart LR req["Request /book?id=99"] --> exc["exception interceptor (first in the stack)"] exc --> others["other interceptors, then the action method"] others -->|"returns success"| res["Result: book.jsp"] others -->|"throws BookNotFoundException"| exc2["exception interceptor catches it"] exc2 --> map{"Closest exception-mapping: local first, then global"} map -->|"found: result code notfound"| lookup["Result lookup: local result, then global result"] map -->|"none matches"| rethrow["Rethrow: Dispatcher, then web.xml error-page"] lookup --> out["httpheader result: sendError 404, web.xml error-page renders the page"]

The Struts 1 Ancestor

Struts 1 (see Struts 1 (Classic) & Migration) had the same idea under other names. A global-exceptions block (or an exception child of an action mapping) in struts-config.xml paired an exception type with a message key and a path to forward to, and an optional handler class (a subclass of ExceptionHandler) could be plugged in when a forward was not enough. The Struts 2 version is purely declarative: the "handler" is the result type, so a redirect, a chain, a stream or an HTTP error are all available without writing a class. The snippet is a labelled "before" example and is not used anywhere else on this page.

Before: Struts 1 global-exceptions in struts-config.xml (historical)
<global-exceptions>
    <exception key="error.catalogue" type="java.sql.SQLException" path="/error.jsp"/>
</global-exceptions>

The Elements

Everything lives in struts.xml, in a package that extends struts-default (which already registers the interceptor and puts it first in the stacks). The element order is fixed by the DTD: inside package the sequence is result-types, interceptors, default-interceptor-ref, default-action-ref, default-class-ref, global-results, global-allowed-methods, global-exception-mappings and then the action elements, so global-results must come before global-exception-mappings. Inside an action the children are param, result, interceptor-ref and exception-mapping in any order, followed by an optional allowed-methods.

Table 1. Elements and attributes that take part in exception handling (7.4.0 struts-6.0.dtd, the newest DTD)
Element Attributes and content Meaning

exception-mapping

exception (required, fully qualified class name), result (required, a result code), name (optional, implied), zero or more param children

Maps an exception class and its subclasses to a result code. Allowed inside action (local mapping) and inside global-exception-mappings (global mapping). The param children are appended to the invocation’s parameters

global-exception-mappings

one or more exception-mapping children

Mappings that apply to every action of the package and of the packages that extend it

global-results

result children

Results every action of the package can reach, including the ones named by exception mappings

result

name, type, param children

Local results are tried first, then global results, exactly as for a code returned by execute()

interceptor-ref name="exception"

param name="exception.logEnabled" and the other logging parameters

Activates the mapping and its logging; present in every stack of struts-default

Worked Example: Global Mappings for the Bookshelf

The Bookshelf action book looks a book up by its id and throws in three situations: a missing book (BookNotFoundException, a checked exception that extends Exception), a database that is down (CatalogUnavailableException, a runtime exception that wraps an SQLException), and a bug (IllegalStateException).

BookDetailAction.java
public class BookDetailAction extends ActionSupport {
    private static final Map<Integer, String> TITLES = Map.of(1, "Dune", 2, "Neuromancer");

    private int id;
    private String title;

    @StrutsParameter
    public void setId(int id) { this.id = id; }

    public String getTitle() { return title; }

    @Override
    public String execute() throws Exception {
        if (id == 500) {
            throw new CatalogUnavailableException("catalog down",
                    new SQLException("connection refused: jdbc:postgresql://db.internal:5432/books user=bookshelf"));
        }
        if (id == 13) {
            throw new IllegalStateException("unlucky book");
        }
        title = TITLES.get(id);
        if (title == null) {
            throw new BookNotFoundException("No book with id " + id);
        }
        return SUCCESS;
    }
}

The configuration maps the three exceptions to three result codes. The specific ones answer with an HTTP error through the httpheader result (its error parameter calls sendError, as the result’s documentation describes) and the general java.lang.Exception mapping renders a neutral page.

struts.xml (excerpt): the exception handling of the Bookshelf
<package name="default" extends="struts-default" namespace="/">

    <global-results>
        <result name="error">/WEB-INF/views/error.jsp</result>
        <result name="unavailable" type="httpheader">
            <param name="error">503</param>
            <param name="errorMessage">Catalogue unavailable</param>
        </result>
        <result name="notfound" type="httpheader">
            <param name="error">404</param>
            <param name="errorMessage">No such book</param>
        </result>
    </global-results>

    <global-exception-mappings>
        <exception-mapping exception="org.example.bookshelf.CatalogUnavailableException" result="unavailable"/>
        <exception-mapping exception="org.example.bookshelf.BookNotFoundException" result="notfound"/>
        <exception-mapping exception="java.lang.Exception" result="error"/>
    </global-exception-mappings>

    <action name="book" class="org.example.bookshelf.BookDetailAction">
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>

The error.jsp page is deliberately static, and the web.xml of the application declares what the container renders for the HTTP errors that the httpheader result sends (the specification of that mechanism is the subject of Servlet Fundamentals and of the Pages error-handling page).

web.xml (excerpt): the container’s error pages
<error-page>
    <error-code>404</error-code>
    <location>/WEB-INF/views/fallback.jsp</location>
</error-page>
<error-page>
    <error-code>503</error-code>
    <location>/WEB-INF/views/fallback.jsp</location>
</error-page>
<error-page>
    <exception-type>java.lang.Throwable</exception-type>
    <location>/WEB-INF/views/fallback.jsp</location>
</error-page>

Calling the application gives the following responses.

$ curl -s -i 'http://localhost:18080/g8exc/book?id=1'
HTTP/1.1 200
<h1>Dune</h1>

$ curl -s -i 'http://localhost:18080/g8exc/book?id=99'      # BookNotFoundException, notfound result
HTTP/1.1 404
<h1>Fallback error page</h1>
<p>Status: 404</p>

$ curl -s -i 'http://localhost:18080/g8exc/book?id=500'     # CatalogUnavailableException, unavailable result
HTTP/1.1 503
<h1>Fallback error page</h1>
<p>Status: 503</p>

$ curl -s -i 'http://localhost:18080/g8exc/book?id=13'      # IllegalStateException, general Exception mapping
HTTP/1.1 200
<h1>Something went wrong</h1>
<p>The request could not be completed. Please try again later.</p>

The last answer shows the first pitfall of the plain dispatcher result: the page is correct but the status is 200. A monitoring tool, a cache or a search engine believes the request succeeded. The pattern used for the first two exceptions is better: let the exception result send the HTTP error and let the container’s error-page render it, so the status and the body agree (see Status Codes: Do Not Return 200 for a Failure).

Which Mapping Wins

The interceptor does not simply read the mappings in order. The documentation says it "will find the closest class inheritance match between the Exception thrown and the Exception declared" and that it examines the local mappings first and then the global ones. The bytecode of the 7.4.0 interceptor shows what that means: it builds one list (local mappings, then global mappings), computes for every entry the number of superclass steps from the thrown class up to the declared class, and keeps the entry with the smallest distance; on a tie the earlier entry, that is the local one, is kept. A declared class that is not in the hierarchy of the thrown exception is skipped.

Table 2. How the interceptor ranks the mappings for a thrown exception
Situation Result Why

Local Exception and global Exception, IllegalStateException thrown

the local result

Both are 2 steps away (IllegalStateException, RuntimeException, Exception); the tie goes to the local mapping. This is the "action-specific mapping takes precedence" rule of the Getting Started tutorial

Global BookNotFoundException and global Exception, BookNotFoundException thrown

notfound

0 steps beats 1 step

Local Exception and global CatalogUnavailableException, CatalogUnavailableException thrown

unavailable (the global one)

0 steps beats 2 steps. A broad local mapping does not shadow a more specific global one

Local BookNotFoundException and global BookNotFoundException

the local result

Same distance, local first

Only unrelated classes are mapped

no result: the exception is rethrown

Nothing matches, so the container sees the exception (see What the Mapping Does Not Catch)

The "broad local does not shadow a specific global" row is not obvious from the guide, which speaks of "first local and then global"; it was verified with the leaky action of the scratch application (local mapping java.lang.Exception, thrown CatalogUnavailableException), which answered 503 through the global unavailable result while the same action rendered its local result for the IllegalStateException of id=13.

A local mapping is declared inside the action. The Bookshelf lend action treats an unknown book as a form problem, not as a 404, and so overrides the global mapping for the same exception; it also passes a param to show what that child element does.

struts.xml (excerpt): a local mapping with a parameter
<action name="lend" class="org.example.bookshelf.LendAction">
    <exception-mapping exception="org.example.bookshelf.BookNotFoundException" result="input">
        <param name="reason">unknown</param>
    </exception-mapping>
    <result>/WEB-INF/views/lend.jsp</result>
    <result name="input">/WEB-INF/views/lend-input.jsp</result>
</action>
lend-input.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<p>Unknown book: <s:property value="exception.message"/> (param: <s:property value="#parameters.reason"/>)</p>
$ curl -s -i 'http://localhost:18080/g8exc/lend?id=7'
HTTP/1.1 200
<p>Unknown book: No book with id 7 (param: unknown)</p>

The param children of an exception-mapping are appended by the interceptor to the invocation’s parameters, which is why the expression #parameters.reason finds the value. The Convention plugin documents the same thing for its annotation ("the parameters defined by params are passed to the result").

From the Code to the Result

After the interceptor has chosen a mapping it returns the mapped code instead of the exception, and the framework treats it like any other result code. The Core Developers Guide states the rule: it first looks for the result in the local action mapping, and if not found, for a global result. A mapping that names a code with no result anywhere is a configuration error, reported as "No result defined for action …​ and result …​", and that new exception is no longer inside the interceptor’s try block (see the matrix in What the Mapping Does Not Catch). Any result type can be the target: a dispatcher JSP, a redirect to a login page, a redirectAction, a chain to an action that prepares an error page, an httpheader that sends an HTTP error. The Core Developers Guide’s own example mixes a redirect for a security exception and a chain for an SQLException.

The Bookshelf chain variant maps the database failure to a maintenance action instead of an HTTP error.

struts.xml (excerpt): exception mapped to a chain result
<action name="chained" class="org.example.bookshelf.BookDetailAction">
    <exception-mapping exception="org.example.bookshelf.CatalogUnavailableException" result="maint"/>
    <result name="maint" type="chain">
        <param name="actionName">maintenance</param>
    </result>
    <result>/WEB-INF/views/book.jsp</result>
</action>
<action name="maintenance">
    <result>/WEB-INF/views/maintenance.jsp</result>
</action>
$ curl -s -i 'http://localhost:18080/g8exc/chained?id=500'
HTTP/1.1 200
<h1>Maintenance</h1>

The exception and exceptionStack Values

When a mapping is found the interceptor calls publishException, whose default implementation wraps the exception in an org.apache.struts2.interceptor.ExceptionHolder and pushes that holder onto the value stack. The holder has exactly two getters, so a result can read two names:

Table 3. Values the exception interceptor puts on the value stack
Name Holder getter Content

exception

getException()

The exception object. <s:property value="exception"/> prints its toString(), and exception.message its message

exceptionStack

getExceptionStack()

The stack trace as text, including the Caused by chain

Both exist only for the result that follows a mapped exception, which is why a JSP that prints them is empty when it is rendered by an action that did not throw. The 7.4.0 bytecode of the interceptor registers the holder and the exception class with the OGNL thread allowlist (ThreadAllowlist.allowClassHierarchy) when it publishes them, which is what keeps these expressions working under the Struts 7 allowlist. Subclasses can change what is published by overriding publishException(ActionInvocation, ExceptionHolder); the documentation suggests adding logging there.

error.jsp that prints the stack: the page that must not ship
<%@ taglib prefix="s" uri="/struts-tags" %>
<h1>Error</h1>
<p><s:property value="exception"/></p>
<pre><s:property value="exceptionStack"/></pre>

That is the Getting Started tutorial’s error.jsp almost word for word, and it is the example the next section tells you not to use in production. Requesting leaky?id=13 with it returns:

$ curl -s 'http://localhost:18080/g8exc/leaky?id=13'
<h1>Error</h1>
<p>java.lang.IllegalStateException: unlucky book</p>
<pre>java.lang.IllegalStateException: unlucky book
	at org.example.bookshelf.BookDetailAction.execute(BookDetailAction.java:30)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at ognl.OgnlRuntime.invokeMethodInsideSandbox(OgnlRuntime.java:909)
	...

The leaked trace names the class and line of the action, the OGNL internals, the Struts version’s package layout and, for the CatalogUnavailableException, a Caused by line that would contain the JDBC URL and user name. That is the information an attacker needs to choose an exploit, and the reason for the next two sections.

Logging What Was Handled

By default a handled exception is converted and forgotten: a request to an action whose stack has the plain exception interceptor produced no log line in the scratch application. Logging is switched on with three interceptor parameters.

Table 4. Parameters of the exception interceptor (7.4.0)
Parameter Default Meaning

logEnabled

false

When true, every exception the interceptor handles is also logged, with its stack trace

logLevel

debug

The level used: trace, debug, info, warn, error or fatal

logCategory

the interceptor’s class

The logger category; set it to route the entries to your own appender (for example bookshelf.errors)

The parameters are given to the interceptor by name inside a stack, with the interceptor’s name as prefix. The tutorial builds a stack that wraps defaultStack and makes it the package default, so every action logs its handled exceptions at ERROR:

struts.xml (excerpt): logging handled exceptions
<interceptors>
    <interceptor-stack name="bookshelfStack">
        <interceptor-ref name="defaultStack">
            <param name="exception.logEnabled">true</param>
            <param name="exception.logLevel">ERROR</param>
            <param name="exception.logCategory">bookshelf.errors</param>
        </interceptor-ref>
    </interceptor-stack>
</interceptors>
<default-interceptor-ref name="bookshelfStack"/>

With the Log4j2 default configuration (the log4j-slf4j2-impl binding of the scratch application) one request produced, on the Tomcat console, the following; the user sees only the 503 page while the operator reads the cause with the connection string:

2026-10-08T13:48:43.581456Z http-nio-18080-exec-822 ERROR catalog down
org.example.bookshelf.CatalogUnavailableException: catalog down
	at org.example.bookshelf.BookDetailAction.execute(BookDetailAction.java:26)
	...
Caused by: java.sql.SQLException: connection refused: jdbc:postgresql://db.internal:5432/books user=bookshelf

This is the "log everything with the stack trace in our own file and present a friendly page with no stack trace" pattern that the interceptor’s documentation names as the purpose of the parameters. The log is where secrets in exception messages belong, and the log file must be protected accordingly. Which logger writes and in which format is a logging-framework question; the Struts side (Log4j2 as the default binding, categories, levels) is covered on the debugging and logging page of this part.

Error Pages: Three Layers

Three mechanisms can produce the page the visitor sees. They nest, and a careful application uses all three.

Table 5. The three layers of error pages in a Struts application
Layer Configured in Handles

1. Exception mapping and result

struts.xml: exception-mapping, global-results

Exceptions thrown by the action method and by the interceptors inside the exception interceptor, and exceptions thrown while a result renders. The page can use the value stack, the action and the full Struts tag library

2. Dispatcher

struts.handle.exception (default true)

Anything that escapes layer 1 (no mapping, an exception outside the interceptor, an unknown action). With true the dispatcher handles it by calling sendError(); with false it rethrows it as a ServletException for other layers, such as Spring Security, to process

3. Container

web.xml: error-page by error-code or exception-type

HTTP errors sent by sendError, including the ones from the httpheader result, and exceptions that reach the container. The page is a plain JSP or static file and sees the jakarta.servlet.error.* attributes, not the value stack

An exception thrown while a result renders is inside layer 1: a result JSP of the scratch application that throws before its buffer is flushed was mapped by the general java.lang.Exception mapping and the visitor received error.jsp; the text written before the exception was discarded with the buffer. Once the response is committed nothing can change the page, which is the same limit as in plain JSP error handling.

Status Codes: Do Not Return 200 for a Failure

A dispatcher result renders a JSP with whatever status the response has, normally 200. There are three ways to give a failure the correct status, in order of preference.

  1. Send the error and let the container render it. The result is an httpheader with the error parameter (and an optional errorMessage), and web.xml declares an error-page for that code. This is the pattern used for 404 and 503 above, and the status of the response and the body agree.

  2. Set only the status. The httpheader result also has a status parameter that sets the status without calling sendError; it has no body of its own, so it suits 204, 201 and redirect-like answers rather than error pages.

  3. Set it in the action. An action that implements ServletResponseAware (or reads the response from the ServletActionContext) calls setStatus before returning the code. The JSP then renders with that status.

The tutorial’s approach of a JSP for Exception is correct for the user and wrong for the status, and the scratch application proves it. The strict action below maps any exception to a fatal result that sends a 500:

struts.xml (excerpt): a general failure that returns 500
<global-results>
    <result name="fatal" type="httpheader">
        <param name="error">500</param>
        <param name="errorMessage">Internal error</param>
    </result>
</global-results>

<action name="strict" class="org.example.bookshelf.BookDetailAction">
    <exception-mapping exception="java.lang.Exception" result="fatal"/>
    <result>/WEB-INF/views/book.jsp</result>
</action>
$ curl -s -i 'http://localhost:18080/g8exc/strict?id=13'
HTTP/1.1 500
<h1>Fallback error page</h1>
<p>Status: 500</p>

The cost of the httpheader approach is that the error page of layer 3 cannot use the value stack. For an application that needs a localised or branded error page with Struts tags, map the exception to a chain or redirectAction result that leads to an error action, give that action’s JSP the content, and set the status in the action (way 3).

What the Mapping Does Not Catch

The interceptor wraps one call, invocation.invoke(), in try { ... } catch (Exception e). Everything that happens outside that call, or is not an Exception, bypasses the mappings. The matrix lists what was exercised on Tomcat 11.0.26.

Table 6. What reaches an exception mapping (verified with the scratch application)
Where it is thrown Mapped? What happened

The action method, or code it calls

yes

The cases of this page

A JSP result while rendering (before the buffer is committed)

yes

Mapped to the general Exception result and rendered as error.jsp with status 200

An interceptor placed after exception in the stack

yes

Action first (stack exception, broken): error.jsp

An interceptor placed before exception in the stack

no

Action last (stack broken, exception): status 500 from the container’s error-page, and the dispatcher logged "Exception occurred during processing request"

The action’s constructor

no

Action ctor answered 500 through web.xml; the log reads "Unable to instantiate Action …​ java.lang.reflect.InvocationTargetException". The Core Developers Guide says it explicitly: global exception mappings "are designed to be used with exceptions thrown by action methods"

A mapped code with no result anywhere

no

"No result defined for action …​ and result …​" is raised after the interceptor has finished, and the generic page is shown (with devMode off)

A subclass of Error (for example OutOfMemoryError)

no

The catch clause is for Exception, so errors propagate to the container

An unknown action

no

No action runs. The dispatcher logs "Could not find action or result" and the container answers 404

The interceptor must therefore be first in the stack, and the documentation says as much: "make this interceptor the first interceptor on the stack, ensuring that it has full access to catch any exception, even those caused by other interceptors". In struts-default.xml of 7.4.0 the exception interceptor is the first element of basicStack, paramsPrepareParamsStack and defaultStack, and the other stacks are built on those. A hand-written stack that lists exception second silently loses the first interceptor’s exceptions, which is the last row.

struts.xml (excerpt): the two orderings that were compared
<interceptor-stack name="exceptionFirstStack">
    <interceptor-ref name="exception"/>
    <interceptor-ref name="broken"/>
</interceptor-stack>
<interceptor-stack name="exceptionLastStack">
    <interceptor-ref name="broken"/>
    <interceptor-ref name="exception"/>
</interceptor-stack>
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8exc/first
200
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8exc/last
500

Exceptions, Field Errors and Action Errors

An exception is for the unexpected. Expected failures such as a blank title, a non-numeric id or a password that does not match are not exceptions: type-conversion failures are recorded by the conversionError interceptor, validation adds field errors, and the workflow interceptor returns input when there are any. Those messages are shown with the actionerror and fielderror tags; the Core Developers Guide’s sample error.jsp calls <s:actionerror/> next to the exception. There is a trap in that arrangement that the scratch application exposed: book?id=abc is a conversion error, the workflow interceptor therefore returns input, and the action book has no input result. The resulting "No result defined for action …​ and result input" exception is mapped by the general Exception mapping, so the visitor got the polite error.jsp and the actual mistake (a missing input result) is only in the log. A broad mapping hides configuration errors from the developer; keep the log on and add an input result.

Never Show Stack Traces

The rules, in order of importance:

  1. The response never contains exceptionStack, a stack trace, an exception class name, a SQL statement, a file path or a connection string. The logged entry holds them; the visitor gets a sentence, an optional reference number and the status code. The leaky page above is the example of what not to deploy.

  2. Print exception.message only when you wrote the message for the visitor. The BookNotFoundException text ("No book with id 7") is written for display; the message of a library exception such as an SQLException is not. <s:property> escapes HTML by default (its escapeHtml attribute defaults to true), but a message that includes user input must still never be switched to escapeHtml="false".

  3. struts.devMode is off in production. The Development Mode page opens with the warning to turn it off because it "can expose sensitive data of your application". Its effect on errors is concrete: with struts.devMode=true the scratch application answered the exception of the failing constructor, and a request for a nonexistent action, with HTTP 200 and the "Struts Problem Report" page, which lists the messages, the source file and line and the complete stack trace. A mapped exception such as book?id=13 still rendered its own page; only unmapped problems reach the report.

  4. Keep one fallback. An error-page for java.lang.Throwable and for 404/500 in web.xml (the third layer) is the safety net when the first two are misconfigured; without it Tomcat’s ErrorReportValve writes its own report.

  5. Do not embed request data in the log message without care, and keep secrets out of exception messages in the first place; the log is read by more people than the database is.

The constant struts.handle.exception is documented in default.properties as "Indicates if Dispatcher should handle unexpected exceptions by calling sendError() or simply rethrow it as a ServletException to allow future processing by other frameworks like Spring Security". In the scratch application both settings ended on the same web.xml fallback page for the failing constructor (status 500), so the visible difference appears only with a filter further out that wants to see the exception itself.

struts.xml (excerpt): the two constants
<constant name="struts.devMode" value="false"/>
<constant name="struts.handle.exception" value="true"/>

The Annotation Form

The Convention plugin offers the same mapping as annotations. @ExceptionMapping (with @ExceptionMappings for several) takes exception, result and params, and can sit on the action class, which applies it to every action of that class, or on a method through the exceptionMappings attribute of @Action. The plugin’s documentation shows the shape; it needs struts2-convention-plugin in the project and is covered with the other annotations on the Convention Plugin & Annotations page of this part.

@ExceptionMappings({
    @ExceptionMapping(exception = "java.lang.NullPointerException", result = "success", params = {"param1", "val1"})
})
public class ExceptionsActionLevelAction {
    public String execute() throws Exception {
        return null;
    }
}

Pitfalls

Table 7. Common mistakes with exception handling and global results
Symptom Cause Fix

The mapping is ignored and the container shows its own error

The stack of the action has no exception interceptor (a hand-built stack, emptyStack) or has it after the interceptor that threw

Use defaultStack or a stack that starts with exception

Exception pages return status 200

The result is a dispatcher JSP

Map to an httpheader result with error and give web.xml an error-page for the code, or set the status in the action

The page for a broad Exception appears for a 404-type problem

A missing input or other result, mapped by the general Exception entry

Add the missing result, and check the log: the real exception is there

"No result defined for action …​ and result X"

An exception-mapping names a code that no local or global result declares

Declare the result; global ones go in global-results

The XML fails to load with a DTD or "invalid content" error

global-exception-mappings placed before global-results, or an empty global-exception-mappings (the DTD requires at least one child)

Follow the element order of the DTD

Exceptions thrown by the constructor are not mapped

Mappings apply to action methods and the interceptor chain, not to object creation

Keep constructors trivial; do the work in execute() or prepare()

exception.logEnabled has no effect

The parameter is on an interceptor-ref of a stack that the action does not use, or the stack’s interceptor has another name

Put the parameter on the defaultStack reference with the prefix exception.

A local mapping seems to be ignored for one exception

A global mapping names a class closer to the thrown exception, and the closest class wins whether it is local or global

Map the specific class locally too, or change the global mapping

Sensitive text in the browser

The error JSP prints exceptionStack or a library message; devMode is on

Remove it, keep it in the log, turn devMode off

Old tutorials use servlet-config and com.opensymphony.xwork.interceptor.ExceptionMappingInterceptor

The documentation snippet predates the rename to servletConfig and to org.apache.struts2

Use the names in the 7.4.0 struts-default.xml

logEnabled is true but nothing appears

The logger category’s level in the logging configuration is higher than exception.logLevel

Make the category’s level admit the chosen level

What Changed Recently

Table 8. Changes that matter for exception handling
Change Effect

Struts 7.0: com.opensymphony.xwork2 renamed to org.apache.struts2

The interceptor is now org.apache.struts2.interceptor.ExceptionMappingInterceptor and the holder org.apache.struts2.interceptor.ExceptionHolder; a custom subclass or a logCategory that names the old class must be updated. The "6.x to 7.x migration" guide says a search and replace should be all that is necessary

Struts 7.4.0 (2026-10-02)

The scratch checks above ran on it: the interceptor’s behaviour (closest match, local before global on ties, Exception caught, parameters appended) is as described. The newest published DTD is still struts-6.0.dtd; a struts-7.0.dtd does not exist (the URL answers 404)

Struts 7 OGNL allowlist

The exception interceptor registers the holder and the exception class with the thread allowlist, which keeps exception and exceptionStack usable in results

Struts 7 defaults

struts.devMode=false, struts.handle.exception=true, struts.action.extension=action,,; a project that relied on devMode showing exceptions in the browser must now read the log

Jakarta namespace

Error pages in web.xml use the jakarta.servlet.error.* attribute names (Servlet 5+), and web.xml uses the https://jakarta.ee/xml/ns/jakartaee namespace

Where the Books and the Docs Are Outdated

The four books the section consulted for concept coverage predate this version, and the official pages still carry traces of the history. The remarks are about what changed, not about the text of any book.

Table 9. Where printed and online material no longer matches Struts 7.4.0
Source of the advice What is outdated

Struts 1 book: global-exceptions, ExceptionHandler, RequestProcessor extension

The mechanism is now a declarative exception-mapping plus a result type; there is no handler class to write. struts-config.xml, ActionForward and the .do mapping no longer exist in the Struts 2 line

Struts 2.0 era material (package com.opensymphony.xwork2, struts-2.0.dtd, global results)

Packages moved to org.apache.struts2 in 7.0 and struts-6.0.dtd is the current DTD; the mechanism of global results is unchanged, but any dispatcher JSP error page it shows returns status 200 (see Status Codes: Do Not Return 200 for a Failure)

Core Developers Guide, "Snippet of struts-default.xml"

Shows com.opensymphony.xwork.interceptor.ExceptionMappingInterceptor, the stack name servlet-config and the stack without httpMethod, csp, datetime, multiselect and actionMappingParams. 7.4.0 uses org.apache.struts2.interceptor.ExceptionMappingInterceptor and the camel-case name servletConfig

Core Developers Guide, struts.xml snippet

Closes the <struts> root with </xwork>, which is not well-formed against the current DTD, and declares the package without extends

Exception interceptor page

Gives the default log category as com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor; in 7.4.0 the class lives in org.apache.struts2.interceptor, and the example uses extends="xwork-default" instead of struts-default

Getting Started tutorial

Its error.jsp prints exceptionStack to the browser; fine for a tutorial, wrong for production

Any text that shows *.do URLs or struts.xml with the xwork DOCTYPE

The default extension is action (and none), the DOCTYPE is -//Apache Software Foundation//DTD Struts Configuration 6.0//EN

Summary

Table 10. Exception handling cheat list
Task How

Turn an exception into a page

exception-mapping (local in action, or in global-exception-mappings) plus a result

Reach the page from every action

global-results before global-exception-mappings; the exception interceptor first in the stack

Get the right status

httpheader result with error, plus error-page in web.xml

Log handled exceptions

exception.logEnabled, exception.logLevel, exception.logCategory on the interceptor-ref

Show the exception to the page

exception and exceptionStack on the value stack, only for mapped exceptions, never to the visitor in production

Catch unmapped problems

error-page for java.lang.Throwable, 404 and 500; struts.handle.exception; struts.devMode off

Annotations

@ExceptionMapping and @ExceptionMappings of the Convention plugin