Where a Failure Goes
Raise typed errors, share a public handler and trace Continue and Propagate through their owning scopes.
The customer lookup for order A-1001 times out. The flow catches the error, writes {"error":"customer service unavailable"} to the payload, and returns HTTP 200. The JSON is well formed. The response still tells the wrong story.
An error handler makes two separate decisions — whether its owning scope succeeds or fails, and what information leaves the application. A fallback can legitimately count as a successful scope result; a failed order must still have an honest public status and body. Confusing the two decisions is how a timeout becomes an apparently successful checkout.
An invalid order is a condition we can identify ourselves. A failed connection or an unavailable database arrives from another operation. Mule represents both as typed errors, and an error handler decides what the owning scope does with them.

Open this checkpoint in ACB
Stop the previous application. Open book/checkpoints/08-error-boundaries from the companion as the project folder, then open src/main/mule/app.xml. Use Flow List to select the named example. Run Run Mule Application from Run and Debug; wait for deployment before sending requests. After a canvas edit, use Save and Hot-deploy to Local Runtime.
The HTTP verification command, run from the companion root while this checkpoint is running, is python3 book/run.py verify 08. It checks the supplied baseline; restore exercise changes before using it.
Raise a named application error
The rule below is the quantity rule from the previous chapter, unchanged, because keeping the input familiar makes the change in failure handling easier to see. What changes is what happens when the rule fails: instead of selecting a response inline, the subflow raises a named error and leaves the response to a handler.
Example 017 — Raise an invalid-order error
The complete source is in checkpoints/08-error-boundaries/src/main/mule/app.xml.
Choose Flow List → validate-order. This subflow contains a Choice. Its first condition negates the complete valid-order predicate from chapter 7, so it selects invalid orders. Expand that route and select Raise Error: Type is APP:INVALID_ORDER, and Description is Order must contain positive integer quantities.
The Otherwise route contains a Logger with Level DEBUG and Message Order structure accepted by the local rule. It changes neither the payload nor the validation outcome.
This subflow raises APP:INVALID_ORDER when the rule fails. APP is the application’s error namespace and INVALID_ORDER names this failure. A namespace of your own is what distinguishes a failure the application recognizes from a connector-defined type. The description helps a developer understand the cause — it is not automatically the public API response.
The otherwise branch has only a DEBUG log message. It lets a valid event continue unchanged. The subflow’s task is validation, so it should not replace a valid order with a success envelope before a later operation needs the order itself.
The caller invokes that subflow and constructs its response only if validation succeeds:
Example 018 — Handle validation at the request boundary
The complete source is in checkpoints/08-error-boundaries/src/main/mule/app.xml.
Choose Flow List → typed-validation. Follow Listener → Flow Reference → Transform Message. The Listener path is /validate, and the Flow Reference Name is validate-order.
Expand the flow’s Error handler area. Select its configuration and set Ref to Public_Errors. The Listener’s normal and error status expressions remain vars.httpStatus default 200 and vars.httpStatus default 500. The success Transform Message uses this script:
%dw 2.0
output application/json
---
{orderId: payload.orderId, valid: true}
A raised error stops the normal path at that point. The transform after Flow Reference does not run for an invalid order. The caller’s handler reference names the policy that supplies the failure response.
Put the public error contract in one handler
A service URL, a SQL statement or a nested exception can help an operator, but none of them helps Dana correct her order. The public response needs a stable code and a safe explanation; the diagnostic detail belongs internally. Keeping that contract in one handler is what stops each flow from inventing its own answer.
Example 019 — Share the public error handler
The complete source is in checkpoints/08-error-boundaries/src/main/mule/app.xml.
Open the canvas Global Configurations view and locate Public_Errors. This is a named error handler, shared by flows. Inspect its ordered handlers and their child processors:
| Handler | Type | Set Variable | JSON payload from Transform Message |
|---|---|---|---|
| On Error Propagate, first | APP:INVALID_ORDER | httpStatus = 400 | {error: "INVALID_ORDER"} |
| On Error Propagate, second | ANY | httpStatus = 500 | {error: "INTERNAL_ERROR"} |
Use expression mode for the numeric variable values. Each Transform Message starts with %dw 2.0, uses output application/json, and places its object below ---. Keep the specific handler before the fallback when editing the handler structure.
The specific APP:INVALID_ORDER handler appears before ANY. It chooses status 400 and a small stable error body. The fallback chooses status 500 and an internal-error code, and it does not serialize the database exception, connector details or an entire Mule error object into the response. ANY is the broad category for handleable errors — not a promise to catch every failure in the runtime, since critical conditions such as a fatal JVM error sit outside ordinary On-Error handling.
on-error-propagate makes its owner finish with an error. The HTTP Listener’s error-response configuration uses the status variable and current payload to form the public response. In this flow, the client should receive 400 and {"error":"INVALID_ORDER"} for invalid input.
The checkpoint also selects Public_Errors as its global default error handler. That supplies a default policy for flows without their own handler. The request flow names the handler explicitly so its boundary is visible when you read it, and a nested Try can still have a different recovery policy. What none of it covers is the runtime itself — a startup or system error, such as a connector configuration that prevents the application from deploying, is not something a flow handler can repair.
Error types form a hierarchy, so a handler can catch one particular connector failure or a broader parent type. Match the documented types the operation you are using actually emits: a timeout, a not-found response and an invalid local order need not have the same public meaning. An absent customer can be a definite business answer, while an unreachable customer service means the application could not obtain an answer — return “customer does not exist” for both and an infrastructure incident turns into false business data. Handler ordering matters for the same reason, because a broad match placed first consumes a failure before a more specific handler sees it.
Continue completes the owner
Neither handler jumps back to the failed processor. Continue does not mean retry, and Propagate does not mean undo everything the flow has done. The difference between them is entirely about which scope owns the handler, and the cheapest way to see that is a flow that fails on purpose.
Example 020 — Recover inside a Try
The complete source is in checkpoints/08-error-boundaries/src/main/mule/app.xml.
Choose Flow List → recover-optional and expand Try. Its first processor is Raise Error with Type APP:OPTIONAL; its next processor is Set Payload with text unreachable.
Expand Try → Error handler and select On Error Continue. Its Type is APP:OPTIONAL, and its child Set Payload supplies fallback. The Transform Message after Try is outside this handler and outside the Try body. Select it and inspect the following script:
%dw 2.0
output application/json
---
{result: payload, reachedAfterTry: true}
Request /optional and inspect the response:
{"result":"fallback","reachedAfterTry":true}
The injected error skips the remaining normal processor inside Try, including the Set Payload with value unreachable. The Try’s Continue handler supplies fallback. Because the owner completes successfully, the surrounding flow proceeds after Try and constructs the final object.
Continue does not resume at the failed processor. It completes the scope that owns the handler. That distinction is easiest to see by marking three positions: before failure, after failure inside Try, and after Try. Only the third is reached after this recovery. Move the same handler out to the flow and the flow itself finishes with the handler’s result instead — so the continuation point travels with the owner, which makes this a change to the structure of the flow rather than the placement of some logging.
Propagate has a different consequence. It leaves the owning scope failed so an enclosing boundary can handle the error. A flow-level Continue can therefore produce a normal response after a nested scope failed — and that says nothing by itself about whether database writes committed. The transaction chapter inspects the database independently instead of inferring persistence from a response message.
Choose recovery from the operation’s meaning
An optional shipping estimate can be unavailable while an order view remains useful. A failed mandatory payment cannot be replaced with the same success response. The handler should express the service contract, not merely make the red component disappear — and a recovery policy that fits an optional estimate becomes dishonest the moment the same shape is applied to a stock reservation the flow then confirms.
For an optional dependency, an explicit unavailable value is often clearer than zero. Zero shipping cost looks like free shipping; zero delivery days looks like immediate delivery. Name the unavailable state so a reader of the response can distinguish it from an actual estimate.
Logging and error handling have different responsibilities. The public response provides a stable category and useful client guidance. Internal diagnostics retain enough context to investigate. Correlation identifiers can connect the two without exposing exception details to the client.

Try scope with its On Error Continue type set to APP:OPTIONAL.
Try it
1. Trace the skipped processor. In Recover inside a Try, why is unreachable never returned?
Show answer
The raised error transfers control to the Try’s error handler. Continue completes that Try; it does not return to the next normal processor inside it. The outer transform receives the handler’s fallback payload.
2. Move the broad handler. What happens if ANY precedes the invalid-order handler?
Show answer
The broad handler can match the application error first, so the intended 400 response is replaced by the fallback policy. Keep specific cases before the catch-all and test the actual status and body.
3. Choose the owner. A lookup is optional but saving the order is mandatory. Should one Continue handler hide either failure?
Show answer
No. Give the optional lookup a local recovery boundary whose result explicitly means unavailable. Let a failed mandatory save fail the acceptance operation. The scope arrangement should follow those two different promises.
With error boundaries in place, we can make a real outbound call and decide what its result means for the current order.
Comments