All articles

Power Automate Error Handling That Actually Works

Run After, scopes, and terminate actions are not enough. Here's the error handling architecture I use in every production flow after learning it the hard way.

· 10 min read

I’ll be honest: the first production flow I ever built had zero error handling. It ran fine for three weeks, then one day a SharePoint permission change caused it to silently fail — and nobody noticed for two days because there were no alerts, no logs, no nothing. Just silence.

That was an expensive lesson. This article is the error handling architecture I’ve landed on after years of building flows that actually need to work.

Why “Configure Run After” Isn’t Enough

The first thing people learn about error handling in Power Automate is the Configure Run After setting. Right-click an action, set it to run when the previous action fails. Done, right?

Not really. The problem is that this approach leads you toward a flat, linear structure where every action needs individual failure handling. It doesn’t scale, it’s hard to read, and it doesn’t give you a central place to collect error context.

The architecture that works — and that I’ve seen used in serious enterprise flows — is the scope + parallel branch pattern.

The Scope + Parallel Branch Pattern

The idea is simple:

  1. Wrap your entire flow logic in a Scope action called something like TRY
  2. Add a Parallel Branch after it with a second scope called CATCH
  3. Configure the CATCH scope to run only when TRY has failed
graph TD A["Initialize Variables"] --> B["Scope: TRY\n(your flow logic lives here)"] B -->|"TRY succeeds"| D["Continue flow"] B -->|"Run After: TRY has failed"| C["Scope: CATCH\n(error handling logic)"]

Why scopes? Because a Scope action fails if any action inside it fails. So the entire TRY block becomes a single point of failure detection, and your CATCH block runs as a unified handler.

What Goes Inside the CATCH Block

This is where most guides stop — they show you the pattern but don’t tell you what to actually put in the handler. Here’s what I include:

1. Capture the error details

The result() function gives you the output of every action inside a scope:

result('TRY')

This returns an array of action results. Each entry has a status property (Succeeded or Failed) and an outputs object with error details. To extract the first failed action’s error message, use a Filter array action:

Filter: result('TRY')
Where: @equals(item()?['status'], 'Failed')

Then get the error message from the filtered result:

@{first(body('Filter_array'))?['error']?['message']}

If you want a simpler one-liner without the filter (useful for quick setups), you can use actions('TRY')?['status'] to check if the scope failed, and combine it with a descriptive varCurrentOperation variable to identify which step went wrong.

Store the error in a string variable initialized at the start of your flow: varErrorMessage.

2. Send a meaningful alert

An HTTP action or Send Email action. The key is what information to include:

  • Flow name: @{workflow()['name']}
  • Run ID: @{workflow()['run']['name']} — this links directly to the run history
  • Timestamp: @{utcNow()}
  • Error message: your captured variable
  • Environment: hardcode this — “Production”, “UAT”, etc.

A link directly to the failed run is invaluable. You can construct it like this:

https://make.powerautomate.com/environments/@{workflow()['tags']['environmentName']}/flows/@{workflow()['name']}/runs/@{workflow()['run']['name']}

3. Terminate with “Failed”

End your CATCH scope with a Terminate action set to Failed. Include the error message. This marks the run as failed in the run history — which matters if you have monitoring set up.

Without this, a flow with a CATCH block will show as Succeeded in run history even when it hit an error, because the CATCH branch technically completed successfully. That’s misleading.

Initializing Variables for Context

Before your TRY scope, initialize context variables that your error handler can use:

varCurrentOperation (string) = "Starting"

Then update varCurrentOperation at key points inside your flow:

varCurrentOperation = "Fetching customer records"
varCurrentOperation = "Updating Dataverse"
varCurrentOperation = "Sending confirmation email"

When the error fires, you know exactly which step failed. No hunting through run history.

Handling Specific HTTP Error Codes

If your flow calls external APIs or Dataverse directly via HTTP, you’ll want to handle specific error codes differently. Use a Switch action inside your CATCH block:

Switch on: outputs('HTTP_Action')?['statusCode']

Case 429: → wait and retry (use a Do Until loop)
Case 401: → alert the team (auth issue, needs manual fix)
Case 404: → log and continue (record may have been deleted)
Default:  → full error alert

For 429 (rate limiting), Power Automate doesn’t automatically retry HTTP actions the way it does connector actions. You need to handle this explicitly.

The Retry Policy Trap

Power Automate has a built-in Retry Policy on actions (accessible via settings). The default is 4 retries with exponential backoff. This sounds great until you realize:

  • It retries on all failures, including ones that will never succeed (like a 400 Bad Request)
  • The default exponential backoff can add 20+ minutes to your failure detection time
  • It counts against your action runs quota

My recommendation: set retry policy to None on most actions and handle retries explicitly where you actually need them. This gives you control over what gets retried and when.

Logging to Dataverse

For flows that matter, I log every run to a custom Dataverse table — Flow Run Log — with columns:

ColumnType
Flow NameText
Run IDText
StatusChoice (Success/Failed/Warning)
Error MessageMultiline Text
DurationWhole Number
TimestampDateTime

Create a record at the start of the flow (Status = In Progress), update it at the end. This gives you a queryable history of flow runs that isn’t dependent on Power Automate’s 28-day run history limit.

Putting It All Together

The complete structure:

graph TD A["Initialize Variables\nvarErrorMessage\nvarCurrentOperation\nvarStartTime"] --> B["Scope: TRY\nAll flow logic\n(update varCurrentOperation as you go)"] B -->|"TRY has failed"| C["Scope: CATCH"] C --> D["Set varErrorMessage\n= result('TRY')"] D --> E["Log to Dataverse\nStatus = Failed"] E --> F["Send alert email\nwith run link"] F --> G["Terminate\nFailed, varErrorMessage"] B -->|"TRY succeeds"| H["Actions after TRY\n(only on success)"] H --> I["Log to Dataverse\nStatus = Success"]

Is it more work to set up? Yes. Is it worth it the first time a critical flow fails at 2am and you get an email with a direct link to the failed run, the step that failed, and the exact error message? Absolutely.

Set up the TRY/CATCH pattern once, save it as a template, and use it for every production flow. It takes 10 minutes to set up and saves days of debugging.

Share this article LinkedIn X / Twitter

Related articles