When the Downstream API Doesn't Answer

Mule's error model — error types, the Try scope, and why On Error Continue and On Error Propagate are the two words that decide who sees the failure.

Back in the connectors post, we built a flow that calls a downstream REST API and reshapes its response. It works — right up until the downstream is slow, down, or returns something you didn’t expect. In integration, that isn’t the edge case. It’s Tuesday. The whole job of middleware is to sit between systems you don’t control, and those systems fail on their own schedule.

So the interesting question isn’t “how do I avoid errors.” It’s “when a processor throws, what should the caller see, and does the flow keep going or stop?” Mule has a specific, learnable answer, and it comes down to two phrases you’ll type constantly: On Error Continue and On Error Propagate.

Errors have types, and the types form a tree

When something fails in Mule, the runtime raises an error with a type — a namespaced identifier like HTTP:CONNECTIVITY, HTTP:TIMEOUT, or EXPRESSION. The namespace tells you where it came from (the HTTP connector, the DataWeave engine), and the name tells you what went wrong.

These types aren’t a flat list — they’re a hierarchy. HTTP:CONNECTIVITY is a child of the connector’s MULE:CONNECTIVITY, which is a child of MULE:ANY, the root that matches everything. That matters because when you write an error handler, you name the type you want to catch, and it catches that type and its children. Handle MULE:CONNECTIVITY and you catch every connector’s connectivity failure; handle HTTP:TIMEOUT and you catch only the timeout. Broad at the top, precise at the leaves.

Inside a handler, the failure is available as an error object you read with DataWeave: error.errorType, error.description (the human message), and error.errorType.identifier (just the name, no namespace). You’ll use these to decide what to do and what to say.

The Try scope: a boundary around risky work

By default, an error travels straight up out of the flow. To handle it locally — around one risky processor rather than the whole flow — you wrap that processor in a Try scope. Think of it as the try block you already know: everything inside runs, and if it throws, control jumps to the error handlers you attach underneath.

Here’s the HTTP Request from the connectors post, now wrapped so a failed downstream call doesn’t take the flow down with it:

<flow name="get-user">
    <http:listener config-ref="httpListener" path="/users/{id}"/>
    <try>
        <http:request config-ref="usersApi" method="GET"
                      path="/api/users/{id}">
            <http:uri-params>#[{ id: attributes.uriParams.id }]</http:uri-params>
        </http:request>
        <error-handler>
            <on-error-continue type="HTTP:TIMEOUT, HTTP:CONNECTIVITY">
                <set-payload value='#[{ error: "user service unavailable" }]'/>
            </on-error-continue>
        </error-handler>
    </try>
</flow>

The type attribute takes a comma-separated list, so one handler can cover several related failures. What it does when it catches them is the next decision — and it’s the whole game.

Continue vs. Propagate

Two handlers, one difference.

On Error Continue treats the error as handled. The scope it’s attached to completes successfully, using whatever the handler produced as its result. The flow keeps running as if nothing went wrong. In the example above, a timeout doesn’t blow up the request — the caller gets your { error: "user service unavailable" } payload and the flow moves on. You’ve absorbed the failure and substituted a fallback.

On Error Propagate does the opposite. It runs its processors — logging, cleanup, an alert — and then re-throws, so the error continues climbing to the next handler out. The scope it’s attached to completes with an error. You use this when you want to react to a failure but not pretend it didn’t happen: the caller still needs to know the call failed.

The way I hold it: Continue means “I’ve got this, carry on.” Propagate means “I’ve noted this, now let it fail.” Choosing wrong is the classic beginner bug — swallow an error with Continue when you meant to surface it, and you get a flow that returns 200 OK on top of a broken call, with nobody the wiser until the data is wrong three systems downstream.

Flow-level handlers vs. the Try scope

The Try scope handles errors around a section of a flow. Every flow can also have one flow-level error handler, which catches anything that escapes the flow’s processors — the safety net at the outermost edge:

<flow name="get-user">
    <http:listener config-ref="httpListener" path="/users/{id}"/>
    <http:request config-ref="usersApi" method="GET" path="/api/users/{id}"/>
    <error-handler>
        <on-error-propagate type="ANY">
            <logger level="ERROR" message="#[error.description]"/>
        </on-error-propagate>
    </error-handler>
</flow>

The two compose. Use Try scopes for errors you can meaningfully recover from right where they happen — a fallback value, a retry, an alternate source. Use the flow-level handler for the catch-all: log it, shape a clean response, and let the rest propagate. A common structure is Continue on the specific, recoverable types deep inside, and Propagate on ANY at the flow edge for everything you didn’t anticipate.

Sending back an honest error response

When an error reaches an HTTP Listener that’s still waiting to respond, you usually want a real status code, not a default 500 with a stack trace. The Listener lets you set the error response separately — status, headers, and body — and you build the body with DataWeave off the error object:

<on-error-continue type="HTTP:TIMEOUT, HTTP:CONNECTIVITY">
    <set-payload value='#[output application/json --- {
        error: "upstream unavailable",
        detail: error.description
    }]'/>
    <set-variable variableName="httpStatus" value="503"/>
</on-error-continue>

with the Listener’s error response referencing that variable for its status code. The point isn’t the exact wiring — it’s the discipline: map an internal HTTP:CONNECTIVITY to a 503 Service Unavailable, a bad request body to a 400, a missing record to a 404. Callers of your API should see your error contract, not the leaked internals of whatever you happened to call downstream. Translating between the two is a big part of what your integration is for.

Final thoughts

Error handling in Mule isn’t a chore you bolt on at the end — it’s where an integration earns its keep. The systems you connect will fail, and the value you add is deciding, per failure, whether to recover quietly, surface a clean error, or stop and shout. Get comfortable with the three questions on every risky processor: which error types can it throw, do I Continue or Propagate, and what does the caller deserve to see. Answer those and your flows stop being fragile happy-path demos and start being things you’d trust in front of real traffic.

That trust only counts if you can ship it somewhere other systems can reach. Next, we take the app off your laptop.

Next: Ship It to CloudHub

Comments