Robust error handling is crucial for production MuleSoft applications. This guide covers comprehensive error handling strategies from basic patterns to advanced resilience techniques.
Error Handling Fundamentals
Understanding MuleSoft's error model:
<!-- Basic try-catch-finally pattern -->
<flow name="basic-error-handling-flow">
<http:listener config-ref="HTTP_Listener_config" path="/orders" method="POST"/>
<try doc:name="Try Block">
<!-- Main processing logic -->
<db:insert config-ref="Database_Config" doc:name="Insert Order">
<db:sql>INSERT INTO orders (customer_id, total) VALUES (:customerId, :total)</db:sql>
<db:input-parameters><![CDATA[#[{
customerId: payload.customerId,
total: payload.total
}]]]></db:input-parameters>
</db:insert>
<http:request
method="POST"
config-ref="Inventory_HTTP_Config"
path="/reserve"
doc:name="Reserve Inventory"/>
<error-handler>
<!-- Handle specific database errors -->
<on-error-propagate type="DB:CONNECTIVITY" doc:name="DB Connection Error">
<logger level="ERROR" message="Database connection failed: #[error.description]"/>
<ee:transform doc:name="DB Error Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
errorCode: "DB_CONNECTION_ERROR",
message: "Database temporarily unavailable",
retryAfter: 30,
correlationId: correlationId
}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">503</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
<!-- Handle HTTP request errors -->
<on-error-propagate type="HTTP:CONNECTIVITY, HTTP:TIMEOUT" doc:name="HTTP Error">
<logger level="ERROR" message="External service error: #[error.description]"/>
<ee:transform doc:name="Service Error Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
errorCode: "EXTERNAL_SERVICE_ERROR",
message: "Inventory service unavailable",
retryAfter: 60,
correlationId: correlationId
}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">503</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
<!-- Handle validation errors -->
<on-error-propagate type="VALIDATION:*" doc:name="Validation Error">
<ee:transform doc:name="Validation Error Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
errorCode: "VALIDATION_ERROR",
message: error.description,
details: error.detailedDescription default [],
correlationId: correlationId
}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">400</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
<!-- Catch-all for unexpected errors -->
<on-error-propagate type="ANY" doc:name="Generic Error">
<logger level="ERROR" message="Unexpected error: #[error.errorType] - #[error.description]"/>
<ee:transform doc:name="Generic Error Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
errorCode: "INTERNAL_ERROR",
message: "An unexpected error occurred",
correlationId: correlationId
}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">500</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
</error-handler>
</try>
<!-- Set HTTP status from variable -->
<set-variable variableName="httpStatus" value="#[vars.httpStatus default 200]"/>
</flow>Custom Error Types
Define and raise custom error types:
<!-- Define custom error types in error-types.xml or within flow -->
<flow name="custom-error-types-flow">
<http:listener config-ref="HTTP_Listener_config" path="/customers/{customerId}" method="GET"/>
<!-- Validate customer ID format -->
<choice doc:name="Validate Customer ID">
<when expression="#[attributes.uriParams.customerId matches /^CUST-\d{6}$/]">
<logger level="DEBUG" message="Valid customer ID format"/>
</when>
<otherwise>
<raise-error type="APP:INVALID_CUSTOMER_ID"
description="Customer ID must be in format CUST-XXXXXX"/>
</otherwise>
</choice>
<!-- Query customer -->
<db:select config-ref="Database_Config" doc:name="Get Customer">
<db:sql>SELECT * FROM customers WHERE customer_id = :customerId</db:sql>
<db:input-parameters><![CDATA[#[{
customerId: attributes.uriParams.customerId
}]]]></db:input-parameters>
</db:select>
<!-- Check if customer exists -->
<choice doc:name="Check Customer Exists">
<when expression="#[sizeOf(payload) > 0]">
<ee:transform doc:name="Transform Customer">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
customerId: payload[0].customer_id,
name: payload[0].name,
email: payload[0].email,
status: payload[0].status
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</when>
<otherwise>
<raise-error type="APP:CUSTOMER_NOT_FOUND"
description='Customer not found: $(attributes.uriParams.customerId)'/>
</otherwise>
</choice>
<!-- Check customer status -->
<choice doc:name="Check Customer Status">
<when expression="#[payload.status == 'ACTIVE']">
<logger level="DEBUG" message="Customer is active"/>
</when>
<when expression="#[payload.status == 'SUSPENDED']">
<raise-error type="APP:CUSTOMER_SUSPENDED"
description="Customer account is suspended"/>
</when>
<when expression="#[payload.status == 'CLOSED']">
<raise-error type="APP:CUSTOMER_CLOSED"
description="Customer account is closed"/>
</when>
</choice>
<error-handler>
<on-error-propagate type="APP:INVALID_CUSTOMER_ID">
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
error: "INVALID_CUSTOMER_ID",
message: error.description,
httpStatus: 400
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</on-error-propagate>
<on-error-propagate type="APP:CUSTOMER_NOT_FOUND">
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
error: "CUSTOMER_NOT_FOUND",
message: error.description,
httpStatus: 404
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</on-error-propagate>
<on-error-propagate type="APP:CUSTOMER_SUSPENDED, APP:CUSTOMER_CLOSED">
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
error: error.errorType.identifier,
message: error.description,
httpStatus: 403
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</on-error-propagate>
</error-handler>
</flow>Global Error Handler
Centralize error handling across the application:
<!-- Global error configuration file: global-error-handler.xml -->
<configuration-properties file="error-config.yaml"/>
<global-property name="app.name" value="${app.name}"/>
<global-property name="app.env" value="${app.environment}"/>
<!-- Reusable error handler -->
<error-handler name="Global_Error_Handler">
<!-- Authentication errors -->
<on-error-propagate type="HTTP:UNAUTHORIZED, APIKIT:UNAUTHORIZED">
<flow-ref name="build-auth-error-response"/>
</on-error-propagate>
<!-- Forbidden errors -->
<on-error-propagate type="HTTP:FORBIDDEN">
<flow-ref name="build-forbidden-error-response"/>
</on-error-propagate>
<!-- Not found errors -->
<on-error-propagate type="APIKIT:NOT_FOUND, HTTP:NOT_FOUND">
<flow-ref name="build-not-found-error-response"/>
</on-error-propagate>
<!-- Bad request / Validation errors -->
<on-error-propagate type="APIKIT:BAD_REQUEST, VALIDATION:INVALID_*">
<flow-ref name="build-validation-error-response"/>
</on-error-propagate>
<!-- Method not allowed -->
<on-error-propagate type="APIKIT:METHOD_NOT_ALLOWED">
<flow-ref name="build-method-not-allowed-response"/>
</on-error-propagate>
<!-- Connectivity errors (retryable) -->
<on-error-propagate type="HTTP:CONNECTIVITY, DB:CONNECTIVITY, *:CONNECTIVITY">
<flow-ref name="build-service-unavailable-response"/>
</on-error-propagate>
<!-- Timeout errors -->
<on-error-propagate type="HTTP:TIMEOUT, *:TIMEOUT">
<flow-ref name="build-timeout-error-response"/>
</on-error-propagate>
<!-- Application-specific errors -->
<on-error-propagate type="APP:*">
<flow-ref name="build-app-error-response"/>
</on-error-propagate>
<!-- Catch-all for unexpected errors -->
<on-error-propagate type="ANY">
<flow-ref name="build-internal-error-response"/>
</on-error-propagate>
</error-handler>
<!-- Error response builder sub-flows -->
<sub-flow name="build-auth-error-response">
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
apiVersion: p('api.version'),
error: {
code: "UNAUTHORIZED",
message: "Authentication required",
details: error.description,
timestamp: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"},
correlationId: correlationId,
path: attributes.requestPath default "unknown"
}
}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">401</ee:set-variable>
</ee:variables>
</ee:transform>
<flow-ref name="log-and-track-error"/>
</sub-flow>
<sub-flow name="build-internal-error-response">
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
apiVersion: p('api.version'),
error: {
code: "INTERNAL_SERVER_ERROR",
message: "An unexpected error occurred",
// Don't expose internal error details in production
details: if (p('app.environment') != "prod") error.description else null,
timestamp: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"},
correlationId: correlationId,
path: attributes.requestPath default "unknown"
}
}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">500</ee:set-variable>
</ee:variables>
</ee:transform>
<flow-ref name="log-and-track-error"/>
</sub-flow>
<sub-flow name="log-and-track-error">
<!-- Log error details -->
<logger level="ERROR" doc:name="Log Error">
<ee:message><![CDATA[%dw 2.0
output application/json
---
{
correlationId: correlationId,
errorType: error.errorType.identifier,
errorDescription: error.description,
causeMessage: error.cause.message default null,
stackTrace: if (p('app.environment') != "prod") error.stackTrace else null,
requestPath: attributes.requestPath default "unknown",
httpMethod: attributes.method default "unknown",
timestamp: now()
}]]></ee:message>
</logger>
<!-- Send to error tracking system (e.g., Splunk, DataDog) -->
<async doc:name="Async Error Tracking">
<http:request
method="POST"
config-ref="Error_Tracking_HTTP_Config"
path="/api/errors"
doc:name="Send to Error Tracker">
<http:body><![CDATA[#[{
application: p('app.name'),
environment: p('app.environment'),
correlationId: correlationId,
errorType: error.errorType.identifier,
errorMessage: error.description,
timestamp: now(),
severity: vars.httpStatus match {
case 500 -> "CRITICAL"
case 503 -> "HIGH"
case _ -> "MEDIUM"
}
}]]]></http:body>
</http:request>
</async>
</sub-flow>Retry Strategies
Implement intelligent retry mechanisms:
<flow name="retry-strategies-flow">
<http:listener config-ref="HTTP_Listener_config" path="/process" method="POST"/>
<!-- Simple retry with until-successful -->
<until-successful
maxRetries="${retry.maxAttempts:3}"
millisBetweenRetries="${retry.intervalMs:2000}"
doc:name="Retry External Call">
<http:request
method="POST"
config-ref="External_Service_Config"
path="/api/process"
doc:name="Call External Service">
<http:response-validator>
<http:success-status-code-validator values="200..299"/>
</http:response-validator>
</http:request>
</until-successful>
<!-- Advanced retry with exponential backoff -->
<set-variable variableName="retryCount" value="#[0]" doc:name="Init Retry Counter"/>
<set-variable variableName="maxRetries" value="#[3]" doc:name="Set Max Retries"/>
<set-variable variableName="baseDelayMs" value="#[1000]" doc:name="Set Base Delay"/>
<until-successful maxRetries="#[vars.maxRetries]" millisBetweenRetries="#[1]">
<try doc:name="Retry Block">
<!-- Calculate exponential backoff delay -->
<choice doc:name="Apply Backoff Delay">
<when expression="#[vars.retryCount > 0]">
<ee:transform doc:name="Calculate Delay">
<ee:variables>
<ee:set-variable variableName="delayMs"><![CDATA[%dw 2.0
output application/java
// Exponential backoff: base * 2^retryCount + random jitter
---
vars.baseDelayMs * (2 pow vars.retryCount) + randomInt(500)]]></ee:set-variable>
</ee:variables>
</ee:transform>
<scripting:execute engine="groovy" doc:name="Sleep">
<scripting:code>Thread.sleep(vars.delayMs as Long)</scripting:code>
</scripting:execute>
</when>
</choice>
<set-variable variableName="retryCount" value="#[vars.retryCount + 1]"/>
<http:request
method="POST"
config-ref="External_Service_Config"
path="/api/unreliable"
doc:name="Call Unreliable Service"/>
<error-handler>
<on-error-continue type="HTTP:CONNECTIVITY, HTTP:TIMEOUT"
when="#[vars.retryCount < vars.maxRetries]">
<logger level="WARN"
message="Retry #[vars.retryCount] of #[vars.maxRetries] - Error: #[error.description]"/>
<raise-error type="MULE:RETRY_EXHAUSTED" description="Triggering retry"/>
</on-error-continue>
<on-error-propagate type="HTTP:CONNECTIVITY, HTTP:TIMEOUT">
<logger level="ERROR" message="All retries exhausted"/>
<raise-error type="APP:SERVICE_UNAVAILABLE"
description="Service unavailable after all retries"/>
</on-error-propagate>
</error-handler>
</try>
</until-successful>
</flow>Circuit Breaker Pattern
Implement circuit breaker for fault tolerance:
<!-- Circuit breaker configuration using Object Store -->
<os:object-store
name="Circuit_Breaker_Store"
config-ref="ObjectStore_Config"
persistent="true"
entryTtl="300"
entryTtlUnit="SECONDS"/>
<flow name="circuit-breaker-flow">
<http:listener config-ref="HTTP_Listener_config" path="/protected/call" method="POST"/>
<!-- Check circuit state -->
<flow-ref name="check-circuit-state" doc:name="Check Circuit"/>
<choice doc:name="Circuit State Router">
<when expression="#[vars.circuitState == 'OPEN']">
<!-- Circuit is open - fail fast -->
<raise-error type="APP:CIRCUIT_OPEN"
description="Circuit breaker is OPEN. Service temporarily unavailable."/>
</when>
<when expression="#[vars.circuitState == 'HALF_OPEN']">
<!-- Half-open - allow limited traffic -->
<flow-ref name="execute-with-circuit-tracking" doc:name="Half-Open Call"/>
</when>
<otherwise>
<!-- Circuit closed - normal operation -->
<flow-ref name="execute-with-circuit-tracking" doc:name="Normal Call"/>
</otherwise>
</choice>
<error-handler>
<on-error-propagate type="APP:CIRCUIT_OPEN">
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
error: "SERVICE_UNAVAILABLE",
message: "Service is temporarily unavailable. Please retry later.",
retryAfter: vars.circuitResetTime default 60
}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">503</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
</error-handler>
</flow>
<sub-flow name="check-circuit-state">
<os:retrieve
key="circuit:failure_count"
objectStore="Circuit_Breaker_Store"
target="failureCount"
doc:name="Get Failure Count">
<os:default-value><![CDATA[0]]></os:default-value>
</os:retrieve>
<os:retrieve
key="circuit:last_failure_time"
objectStore="Circuit_Breaker_Store"
target="lastFailureTime"
doc:name="Get Last Failure Time">
<os:default-value><![CDATA[#[0]]]></os:default-value>
</os:retrieve>
<ee:transform doc:name="Determine Circuit State">
<ee:variables>
<ee:set-variable variableName="circuitState"><![CDATA[%dw 2.0
output application/java
var failureThreshold = 5
var resetTimeoutMs = 60000
var timeSinceLastFailure = now() as Number - (vars.lastFailureTime as Number default 0)
---
if (vars.failureCount >= failureThreshold)
if (timeSinceLastFailure > resetTimeoutMs)
"HALF_OPEN"
else
"OPEN"
else
"CLOSED"]]></ee:set-variable>
</ee:variables>
</ee:transform>
<logger level="DEBUG" message="Circuit state: #[vars.circuitState], Failures: #[vars.failureCount]"/>
</sub-flow>
<sub-flow name="execute-with-circuit-tracking">
<try doc:name="Track Circuit Status">
<http:request
method="POST"
config-ref="Protected_Service_Config"
path="/api/endpoint"
doc:name="Call Protected Service">
<http:response-validator>
<http:success-status-code-validator values="200..299"/>
</http:response-validator>
</http:request>
<!-- Success - reset failure count -->
<os:store
key="circuit:failure_count"
objectStore="Circuit_Breaker_Store"
doc:name="Reset Failure Count">
<os:value><![CDATA[0]]></os:value>
</os:store>
<error-handler>
<on-error-propagate type="HTTP:CONNECTIVITY, HTTP:TIMEOUT, HTTP:SERVICE_UNAVAILABLE">
<!-- Increment failure count -->
<os:retrieve
key="circuit:failure_count"
objectStore="Circuit_Breaker_Store"
target="currentFailures"
doc:name="Get Current Failures">
<os:default-value><![CDATA[0]]></os:default-value>
</os:retrieve>
<os:store
key="circuit:failure_count"
objectStore="Circuit_Breaker_Store"
doc:name="Increment Failures">
<os:value><![CDATA[#[vars.currentFailures + 1]]]></os:value>
</os:store>
<os:store
key="circuit:last_failure_time"
objectStore="Circuit_Breaker_Store"
doc:name="Update Last Failure Time">
<os:value><![CDATA[#[now() as Number]]]></os:value>
</os:store>
<logger level="WARN"
message="Circuit failure recorded. Count: #[vars.currentFailures + 1]"/>
</on-error-propagate>
</error-handler>
</try>
</sub-flow>Dead Letter Queue Pattern
Handle failed messages gracefully:
<flow name="message-processing-with-dlq">
<jms:listener config-ref="JMS_Config" destination="orders.queue" doc:name="Listen for Orders"/>
<set-variable variableName="originalPayload" value="#[payload]" doc:name="Store Original"/>
<set-variable variableName="messageId" value="#[attributes.messageId]" doc:name="Store Message ID"/>
<try doc:name="Process Message">
<!-- Validate message -->
<flow-ref name="validate-order-message" doc:name="Validate"/>
<!-- Process order -->
<flow-ref name="process-order" doc:name="Process"/>
<error-handler>
<!-- Transient errors - retry -->
<on-error-continue type="HTTP:CONNECTIVITY, DB:CONNECTIVITY"
when="#[vars.retryCount default 0 < 3]">
<set-variable variableName="retryCount" value="#[(vars.retryCount default 0) + 1]"/>
<logger level="WARN" message="Transient error, scheduling retry #[vars.retryCount]"/>
<!-- Requeue with delay -->
<jms:publish config-ref="JMS_Config" destination="orders.retry.queue" doc:name="Requeue">
<jms:message>
<jms:body><![CDATA[#[vars.originalPayload]]]></jms:body>
<jms:properties><![CDATA[#[{
retryCount: vars.retryCount,
originalMessageId: vars.messageId,
lastError: error.description
}]]]></jms:properties>
</jms:message>
</jms:publish>
</on-error-continue>
<!-- Permanent failures - send to DLQ -->
<on-error-continue type="ANY">
<logger level="ERROR" message="Permanent failure, sending to DLQ: #[error.description]"/>
<ee:transform doc:name="Build DLQ Message">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
originalMessage: vars.originalPayload,
error: {
type: error.errorType.identifier,
description: error.description,
timestamp: now()
},
metadata: {
messageId: vars.messageId,
retryCount: vars.retryCount default 0,
source: "orders.queue",
processedAt: now()
}
}]]></ee:set-payload>
</ee:message>
</ee:transform>
<jms:publish config-ref="JMS_Config" destination="orders.dlq" doc:name="Send to DLQ"/>
<!-- Alert on DLQ -->
<async doc:name="Alert Team">
<http:request
method="POST"
config-ref="Slack_Webhook_Config"
path="/services/webhook"
doc:name="Slack Alert">
<http:body><![CDATA[#[{
text: "⚠️ Message sent to DLQ",
attachments: [{
color: "danger",
fields: [
{title: "Message ID", value: vars.messageId, short: true},
{title: "Error", value: error.description, short: false}
]
}]
}]]]></http:body>
</http:request>
</async>
</on-error-continue>
</error-handler>
</try>
</flow>Conclusion
Effective error handling in MuleSoft requires a layered approach: specific handlers for known errors, global handlers for consistency, retry strategies for transient failures, and circuit breakers for system protection. Implement comprehensive logging and alerting to maintain visibility. These patterns ensure your integrations remain resilient and maintainable in production.