Error Handling Patterns in Logic Apps
Why Error Handling Matters
Production workflows must handle failures gracefully. Azure Logic Apps provides several mechanisms for catching and responding to errors without losing data or leaving systems in an inconsistent state.
Microsoft Reference: Handle errors and exceptions in Logic Apps
Run After Configuration
Every action has a Run After setting that determines when it executes:
| Setting | Description | Use Case |
|---|---|---|
| Is successful | Default — runs only if the previous action succeeded | Normal flow |
| Has failed | Runs only when the previous action fails | Error handling paths |
| Is skipped | Runs when the previous action was skipped | Fallback logic |
| Has timed out | Runs when the previous action timed out | Timeout-specific handling |
You can select multiple conditions to create flexible error handling paths.
Run After in Workflow Definition
{
"Send_error_notification": {
"type": "ApiConnection",
"runAfter": {
"HTTP_Call": [
"Failed",
"TimedOut"
]
},
"inputs": {
"method": "post",
"body": {
"subject": "Workflow Error: @{workflow()?['name']}",
"body": "Action failed with status: @{actions('HTTP_Call')?['status']}"
}
}
}
}
The Scope Pattern (Try/Catch/Finally)
Use Scope actions to implement structured error handling. This is the recommended pattern for production workflows.
Microsoft Reference: Add Scope actions for error handling
Try Block
Scope: "Try"
├── HTTP: Call external API
├── Parse JSON: Process response
├── Condition: Validate response
└── Insert into SQL: Store result
Catch Block
Scope: "Catch" (Run After: Try has failed)
├── Compose: Build error details using result('Try')
├── HTTP: Log error to monitoring endpoint
├── Send Email: Notify operations team
└── Terminate: Mark workflow as Failed
Finally Block
Scope: "Finally" (Run After: Catch is successful, has failed, is skipped)
├── HTTP: Update status tracker
└── HTTP: Release any locked resources
Complete Try/Catch/Finally Workflow Definition
{
"definition": {
"actions": {
"Try": {
"type": "Scope",
"actions": {
"Call_API": {
"type": "Http",
"inputs": {
"method": "GET",
"uri": "https://api.example.com/orders"
},
"retryPolicy": {
"type": "exponential",
"count": 3,
"interval": "PT10S"
}
},
"Process_Response": {
"type": "ParseJson",
"runAfter": { "Call_API": ["Succeeded"] },
"inputs": {
"content": "@body('Call_API')",
"schema": {}
}
}
}
},
"Catch": {
"type": "Scope",
"runAfter": { "Try": ["Failed"] },
"actions": {
"Get_Error_Details": {
"type": "Compose",
"inputs": {
"workflowName": "@workflow()?['name']",
"runId": "@workflow()?['run']?['name']",
"timestamp": "@utcNow()",
"failedActions": "@result('Try')",
"triggerInfo": "@trigger()"
}
},
"Send_Alert": {
"type": "ApiConnection",
"runAfter": { "Get_Error_Details": ["Succeeded"] },
"inputs": {}
}
}
},
"Finally": {
"type": "Scope",
"runAfter": {
"Catch": ["Succeeded", "Failed", "Skipped"]
},
"actions": {
"Update_Status": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://status.example.com/update",
"body": {
"workflowRun": "@workflow()?['run']?['name']",
"status": "@if(equals(result('Try')?[0]?['status'], 'Succeeded'), 'completed', 'failed')"
}
}
}
}
}
}
}
}
Extracting Error Details from Scopes
The result() function returns an array of action results from within a scope:
// Get all results from the Try scope
@result('Try')
// Filter to only failed actions
@filter(result('Try'), item, equals(item['status'], 'Failed'))
// Get the error message from the first failed action
@first(filter(result('Try'), item, equals(item['status'], 'Failed')))?['error']?['message']
// Build a readable error summary
@join(
map(
filter(result('Try'), item, equals(item['status'], 'Failed')),
item,
concat(item['name'], ': ', coalesce(item['error']?['message'], 'Unknown error'))
),
'; '
)
Retry Policies
Configure retry policies on individual actions:
| Policy | Behaviour | Best For |
|---|---|---|
| Default | 4 retries with exponential intervals | General use |
| Fixed interval | Retry N times at a set interval | Predictable recovery times |
| Exponential interval | Increasing delays between retries | Rate-limited APIs, transient failures |
| None | No retries | Non-idempotent operations |
Microsoft Reference: Retry policies
Exponential Retry Configuration
{
"retryPolicy": {
"type": "exponential",
"count": 4,
"interval": "PT10S",
"minimumInterval": "PT5S",
"maximumInterval": "PT1H"
}
}
This produces retry intervals like: 10s → 20s → 40s → 80s (capped at 1 hour).
Fixed Interval Retry
{
"retryPolicy": {
"type": "fixed",
"count": 3,
"interval": "PT30S"
}
}
Retry Behaviour by Status Code
Logic Apps automatically retries on these HTTP status codes:
- 408 — Request Timeout
- 429 — Too Many Requests (respects
Retry-Afterheader) - 500 — Internal Server Error
- 502 — Bad Gateway
- 503 — Service Unavailable
- 504 — Gateway Timeout
Non-retryable errors (400, 401, 403, 404, 409) are not retried regardless of retry policy configuration.
Terminate Action
Use the Terminate action to end a workflow run with a specific status:
| Status | Purpose | Impact |
|---|---|---|
| Succeeded | Mark as successful despite alternative path | Shows green in run history |
| Failed | Mark as failed with custom error code and message | Shows red, triggers alerts |
| Cancelled | Mark as cancelled | Shows grey |
Terminate with Error Details
{
"Terminate": {
"type": "Terminate",
"inputs": {
"runStatus": "Failed",
"runError": {
"code": "ORDER_PROCESSING_FAILED",
"message": "Failed to process order @{triggerBody()?['orderId']}: @{actions('Call_API')?['error']?['message']}"
}
}
}
}
Dead Letter Patterns
For critical workflows, implement a dead letter pattern to ensure no data is lost:
Trigger (Service Bus message)
└── Try Scope
├── Process Message
└── Complete Message
└── Catch Scope (Run After: Try has failed)
├── Log Error Details
├── Send to Dead Letter Queue
└── Complete Original Message (to prevent redelivery)
Dead Letter Queue Configuration
{
"Send_to_Dead_Letter": {
"type": "ApiConnection",
"inputs": {
"method": "post",
"path": "/dead-letter-queue/messages/send",
"body": {
"contentType": "application/json",
"content": "@triggerBody()",
"properties": {
"originalMessageId": "@triggerBody()?['MessageId']",
"errorReason": "@first(filter(result('Try'), item, equals(item['status'], 'Failed')))?['error']?['message']",
"failedAt": "@utcNow()",
"workflowRunId": "@workflow()?['run']?['name']"
}
}
}
}
}
Compensation Patterns
When a multi-step workflow partially fails, use compensation to undo completed steps:
Try Scope
├── Step 1: Create Order (succeeds)
├── Step 2: Reserve Inventory (succeeds)
├── Step 3: Charge Payment (fails)
└── Step 4: Send Confirmation (skipped)
Catch Scope
├── Check: Was inventory reserved?
│ → Yes: Release Inventory (compensate)
├── Check: Was order created?
│ → Yes: Cancel Order (compensate)
└── Notify: Send failure notification
Circuit Breaker Pattern
For Logic Apps calling unreliable external services, implement a circuit breaker using a shared state (Azure Table Storage or Redis):
1. Check circuit state (Table Storage)
2. If OPEN and cooldown not expired → Return cached/default response
3. If CLOSED or cooldown expired → Call external service
4. On success → Reset failure count, set state to CLOSED
5. On failure → Increment failure count
6. If failure count > threshold → Set state to OPEN with cooldown
Tracked Properties
Add tracked properties to actions for custom diagnostics:
{
"trackedProperties": {
"orderId": "@triggerBody()?['orderId']",
"customerId": "@triggerBody()?['customerId']",
"errorCode": "@outputs('HTTP')?['statusCode']",
"correlationId": "@triggerOutputs()?['headers']?['X-Correlation-Id']",
"processingStage": "order-validation"
}
}
These appear in your run history and can be queried via Log Analytics:
AzureDiagnostics
| where ResourceType == "WORKFLOWS"
| where trackedProperties_orderId_s != ""
| project TimeGenerated, trackedProperties_orderId_s, trackedProperties_errorCode_s, status_s
| order by TimeGenerated desc
Microsoft Reference: Monitor Logic Apps with Log Analytics
Diagnostic Settings and Application Insights
Enable Diagnostic Settings
Send Logic Apps telemetry to Log Analytics and Application Insights:
resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
name: 'la-diagnostics'
scope: logicApp
properties: {
workspaceId: logAnalyticsWorkspace.id
logs: [
{
category: 'WorkflowRuntime'
enabled: true
retentionPolicy: { enabled: true, days: 90 }
}
]
metrics: [
{
category: 'AllMetrics'
enabled: true
retentionPolicy: { enabled: true, days: 30 }
}
]
}
}
Key Metrics to Monitor
| Metric | Alert Threshold | Description |
|---|---|---|
| Runs Failed | > 0 in 5 minutes | Workflow execution failures |
| Run Latency | > 30 seconds | Workflow taking too long |
| Actions Failed | > 5 in 15 minutes | Individual action failures |
| Trigger Fire Rate | Drop > 50% | Trigger not firing as expected |
| Billable Executions | Budget threshold | Cost monitoring |
Alerting Patterns
Azure Monitor Alert for Failed Runs
resource failedRunAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
name: 'alert-la-failed-runs'
location: 'global'
properties: {
severity: 1
scopes: [logicApp.id]
evaluationFrequency: 'PT5M'
windowSize: 'PT5M'
criteria: {
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
allOf: [
{
name: 'FailedRuns'
metricName: 'RunsFailed'
operator: 'GreaterThan'
threshold: 0
timeAggregation: 'Total'
}
]
}
actions: [
{ actionGroupId: actionGroup.id }
]
}
}
Best Practices
- Always wrap critical sections in Scope actions for structured error handling
- Configure retry policies on HTTP actions calling external services
- Use Terminate with Failed status to clearly indicate workflow failures
- Send alerts on failure — integrate with Teams, email, or PagerDuty
- Log error context — include correlation IDs, timestamps, and input data
- Implement dead letter patterns for message-driven workflows to prevent data loss
- Use compensation patterns for multi-step transactions that may partially fail
- Enable diagnostic settings to send logs to Log Analytics for long-term analysis
- Add tracked properties to critical actions for custom querying
- Set up Azure Monitor alerts for failed runs and latency thresholds
- Test error paths — deliberately trigger failures during development to verify handling
- Use
result()in Catch scopes to extract detailed error information