Salesforce Flow Error Emails: How to Read and Fix Them
"An unhandled fault has occurred in this flow" means a flow element failed with no fault path connected. The email goes (by default) to the admin who last modified the flow, names the failing element and error, and lists the interview's element-by-element path. To fix it: read the path, reproduce with a debug log, and add fault paths so failures are handled instead of crashing.
Anatomy of the error email
The email contains more diagnostic material than most people read:
- Flow name, version, and interview ID: check the version carefully, because the error may come from an older active version rather than the draft you have open in Flow Builder, which is a common source of "I already fixed that" confusion.
- The error message: e.g. "The flow tried to access a null value", a DML failure with the underlying message (often a validation rule or required field), or "Too many SOQL queries: 101".
- The element path, every element the interview executed, in order, with the failing element last. This is effectively a flow stack trace.
- Variable values at failure (current values of flow variables), often revealing the null that caused everything.
- The triggering record ID and running user: your reproduction recipe.
The variable dump at the bottom is the part most people scroll past, and it frequently contains the entire answer. Scan it for variables holding null where you expected a record, collection variables with a count of zero, and Ids that look wrong for the object involved. A record variable showing null immediately before the failing element confirms the null-access diagnosis without needing a debug log at all.
One caution worth knowing: because these emails contain actual field values, they can carry customer data out to whoever receives them. On orgs handling regulated or personal data, route flow errors to a controlled internal address rather than personal inboxes, and treat the emails as production data rather than routine notifications.
What the user saw, and what actually happened
The email tells you a flow failed. It does not tell you what the failure did to your data, and that depends entirely on where the flow was running.
| Flow type | What the user experienced | Data impact |
|---|---|---|
| Record-triggered (after-save) | Save appeared to fail, or succeeded then reverted | Triggering transaction rolls back |
| Record-triggered (before-save) | Save rejected with an error | Nothing written |
| Screen flow | Generic "unhandled fault" message on screen | Records created earlier in the flow may persist |
| Scheduled flow | Nothing; no user present | That batch fails silently; others continue |
| Autolaunched from Apex | The calling Apex throws | Whole Apex transaction rolls back |
The screen flow row is the one that causes lasting damage. Because a screen flow spans multiple transactions (each screen commits before the next loads), a failure partway through can leave records created by earlier screens in place while later work never happens. The result is orphaned or half-built data that no error message mentions. When triaging a screen flow fault, always check what the earlier elements already wrote.
The scheduled flow row is the one people discover too late. Nobody is watching, so these failures accumulate in an inbox until someone notices the data is wrong. Scheduled flows are the strongest argument for logging faults to a custom object rather than relying on email.
Who gets the email, and how to change it
In Setup → Process Automation Settings, "Send Process or Flow Error Email to" offers:
| Option | Behavior |
|---|---|
| User who last modified the process or flow (default) | Only the last editor knows anything is failing; risky if they've left the team. |
| User who launched the flow | End users see errors for flows they trigger; rarely what you want in production. |
| Apex exception email recipients | Recommended: the list in Setup → Apex Exception Email, which can include multiple admins and external addresses, giving one shared inbox for both Apex and Flow failures. |
Triage: what to do in the first ten minutes
When the emails are arriving faster than you can read them, the instinct is to open one and start debugging. Stop the bleeding first: a failing record-triggered flow is rolling back real users' saves while you investigate.
- Establish the blast radius. Are these emails for one record or hundreds? One is a data problem; hundreds means every save on that object is failing and users are actively blocked.
- Check whether the flow version is new. The email names the flow version. If it matches something deployed today, you have your cause, and rolling back to the previous active version is faster than fixing forward.
- Decide whether to deactivate. For a widespread failure, deactivating the flow version restores service immediately. This is usually right for a flow doing non-critical work, and wrong for one enforcing data integrity, since deactivating it lets bad data through silently.
- Capture one good reproduction. Before changing anything, take one email's record Id and running user and reproduce with a debug log active. Once you deactivate or edit the flow, that evidence is gone.
- Only then diagnose. With the log captured and the bleeding stopped, work through the root-cause steps below without time pressure.
Step 4 is the one most often skipped and most regretted. A reproduction against the exact failing record, with the log captured, answers questions no amount of reading the flow in Flow Builder will, particularly when the cause is another automation rather than the flow itself.
If the failures began without any deployment, suspect the transaction rather than the flow. A new managed package, a new trigger, or simple data growth can push a transaction over a governor limit that the flow was previously fitting inside comfortably.
From email to root cause
- Read the element path bottom-up. The last element failed; the elements before it produced its inputs.
- Check the variable values. A null record variable before a DML or assignment element is the classic culprit (a Get Records found nothing; see Flow debugging for the fix pattern).
- Reproduce with a debug log. The email can't show why a DML failed or what other automation interfered. Activate a trace flag (or use ForceLens Smart Capture), repeat the action on the triggering record, and read the log:
FLOW_ELEMENT_ERRORmarks the failure, and the surroundingVALIDATION_RULE,CODE_UNIT_STARTED(triggers) andWF_RULEevents expose interactions, like a validation rule on a related object blocking the flow's update, or a trigger consuming the SOQL budget before the flow ran (see order of execution). - Check limits. If the message is a limit error, the log's
LIMIT_USAGE_FOR_NSsection shows what consumed the budget — often not the flow itself (governor limits explained).
Decoding the error message
The message in the email is the fastest route to a diagnosis, provided you know what each one usually means. These cover the overwhelming majority:
| Message in the email | What actually happened | Where to fix it |
|---|---|---|
| "The flow tried to access a null value" | A Get Records found nothing; a later element read from the empty variable | Decision check after the Get Records, not at the failing element |
| "FIELD_CUSTOM_VALIDATION_EXCEPTION" | A validation rule rejected the flow's DML | The validation rule, or the data the flow supplies |
| "REQUIRED_FIELD_MISSING" | A Create Records element omitted a required field | Add the field, or supply a default |
| "Too many SOQL queries: 101" | Get Records in a loop, or the transaction was already near its cap | Move the query out of the loop; check other automation |
| "UNABLE_TO_LOCK_ROW" | Two transactions tried to update the same record simultaneously | Usually a concurrency issue, not a flow bug; see below |
| "CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY" | An Apex trigger on the target object threw during the flow's DML | The trigger, not the flow; see below |
Two of these deserve elaboration because they are routinely misdiagnosed.
Validation exceptions are not flow bugs. The flow did exactly what it was told; a rule elsewhere rejected the write. The email surfaces the validation rule's own error text, which is why reading the full message matters: it usually names the rule or the field. The decision to make is whether the flow should supply better data or the rule should exempt this case.
UNABLE_TO_LOCK_ROW is a timing problem. It means another transaction held a lock on the record your flow tried to update. This appears in orgs with heavy automation on parent objects — several child updates each triggering a parent roll-up at once. The flow is a victim rather than a cause, and the fix is usually to reduce contention by moving work asynchronous, not to change the flow.
CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY
The most misleading error in the list, because it names your flow while describing someone else's failure. CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY means the flow's DML reached the database, an Apex trigger on that object then ran, and the trigger threw. The flow is reported as the failure because it owned the transaction, but the bug is in the trigger.
The email usually carries the trigger's own message after the error code, in the form ObjectTrigger: execution of AfterUpdate caused by: System.…. That trailing exception is the real error, and the flow's element path will not explain it.
Spotting it in the log. This is where a debug log settles the argument, because the email cannot show what happened inside the trigger. Find the FLOW_ELEMENT_BEGIN for the failing element, then read forward: you will see DML_BEGIN, then CODE_UNIT_STARTED for the trigger, then the exception inside it.
10:31:02.118|FLOW_ELEMENT_BEGIN|[Update_Account]
10:31:02.140|DML_BEGIN|[EXTERNAL]|Op:Update|Type:Account|Rows:1
10:31:02.166|CODE_UNIT_STARTED|[EXTERNAL]|AccountTrigger on Account trigger event AfterUpdate
10:31:02.201|EXCEPTION_THROWN|[52]|System.NullPointerException: Attempt to de-reference a null object
10:31:02.203|FATAL_ERROR|System.DmlException: Update failed. First exception on row 0; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, AccountTrigger: execution of AfterUpdate
The EXCEPTION_THROWN at line 52 of the trigger is the actual cause. Everything after it is Salesforce reporting the consequence back up the chain to the flow.
Where to fix it. In the trigger, not the flow. That said, the flow can still be complicit: if it updates records the trigger was never designed to receive, or updates them in bulk when the trigger was written for one record, the trigger's assumption is what breaks. Trace the trigger's failure with the debug log, then decide which side should change.
Preventing the crash: fault paths
Every element that can fail (Get/Create/Update/Delete Records, actions, callouts, subflows) has a fault connector, and leaving it unconnected is what turns a recoverable condition into a crash. Connect it to handling logic and the fault becomes handled: no crash, no unhandled-fault email, and (if you choose) a graceful message or a logged error record. A pragmatic pattern:
- Fault path → Create Records on a custom
Flow_Error__cobject (flow name,{!$Flow.FaultMessage}, record ID, user). - Then → a screen with a friendly message that displays
{!$Flow.FaultMessage}(screen flows), so the user can report something specific rather than "it broke", or a silent end (record-triggered flows). - A report or alert on
Flow_Error__cgives the team one dashboard of flow health instead of an inbox of stack traces.
Also add decision checks after every Get Records ("was anything found?"). It is the cheapest insurance against the null-value fault, which is the most common flow error of all.
Building the error log object
Email is a poor monitoring system: it goes to one person, gets filtered, and disappears when they leave. A custom logging object turns the same information into something reportable. A workable minimum:
| Field | Populate with | Why it matters |
|---|---|---|
| Flow Name (Text) | Hard-coded per flow | Group failures by source |
| Error Message (Long Text) | {!$Flow.FaultMessage} | The actual system error |
| Element Name (Text) | Hard-coded on each fault path | Locates the failure without a log |
| Record Id (Text) | The triggering record | Your reproduction recipe |
| Running User (Lookup) | {!$User.Id} | Permission-related failures |
| Occurred At (DateTime) | {!$Flow.CurrentDateTime} | Correlate with deployments and load |
Setting the Element Name manually on each fault path feels tedious and repays the effort immediately: it is the difference between "this flow failed" and "the Update Contacts element failed", which often removes the need to capture a log at all.
One important caveat: in a record-triggered flow, the error record is written inside the same transaction that is about to roll back. If the transaction fails after your fault path runs, the log record disappears with everything else. To guarantee the record survives, write it from an asynchronous path or a subflow running in its own transaction. For screen flows this is not a concern, since each screen commits independently.
With that object in place, a simple report grouped by flow name and a dashboard component showing failures over the last seven days gives the whole team visibility that no inbox can. Add a scheduled report subscription and you have genuine monitoring rather than ad-hoc notification.
When you should not add a fault path
Fault paths are not universally correct. If a record-triggered flow performs work that must succeed for the data to be valid, catching the fault and continuing silently produces corrupted records, a considerably worse outcome than a visible failure and a rollback.
The test is simple: would you rather the save failed, or that it succeeded with incomplete data? If the answer is that it should fail, let the fault propagate and rely on the email. Use the fault path only to log the failure, then leave the flow to end in error rather than routing it to a "success" outcome.
Reviewing flows before they fail
Missing fault paths and unguarded Get Records are exactly the structural issues ForceLens's Flow analyzer flags: hand it any flow from Flow Builder and the Developer and QA lenses list unhandled fault connectors, null-risk references and bulkification concerns — with a health score and a ready .docx report. Cheaper than learning from production emails.
Frequently asked questions
Who receives flow error emails?
By default the admin who last modified the flow. Change it in Setup → Process Automation Settings: "Apex exception email recipients" is the most team-friendly option.
What does "An unhandled fault has occurred in this flow" mean?
An element failed with no fault path attached, crashing the interview and (for record-triggered flows) rolling back the triggering transaction. The email names the element and error and lists the executed path.
How do I find the root cause?
Read the element path and variable values in the email, then reproduce with a debug log; FLOW_ELEMENT_ERROR plus surrounding trigger/validation events show what the email can't.
How do I stop unhandled-fault emails?
Add fault paths to every fallible element. Handled faults don't send the email, and can log to a custom object instead for proper monitoring.
Why am I suddenly getting hundreds of flow error emails?
Usually a data load or bulk update hitting a flow that fails per record. Each failed interview sends its own email. Deactivate the flow version or pause the load first, then diagnose, and add a fault path so the next bulk operation logs rather than mails. If the failures started without a deployment, check whether another automation or package changed the transaction's limit consumption.
Does a flow error roll back the whole transaction?
It depends on the flow type. A record-triggered flow failing rolls back the triggering save. A screen flow is different: each screen commits separately, so records created by earlier screens can persist even though the flow failed later, leaving partial data that the error email does not mention.
Can I send flow error emails to more than one person?
Yes. In Setup → Process Automation Settings, set "Send Process or Flow Error Email to" to Apex exception email recipients, then maintain that list in Setup → Apex Exception Email. It accepts multiple addresses including external ones, so a shared team inbox receives both Apex and Flow failures.
Why does my error log record disappear after the flow fails?
Because in a record-triggered flow the fault path runs inside the transaction that is rolling back, so the log record is discarded with everything else. Write the error record from an asynchronous path or a subflow that runs in its own transaction to guarantee it survives.
The email says a validation rule failed, is that a flow bug?
Usually not. The flow performed its DML correctly and a validation rule rejected the write. The email includes the rule's own error text, which normally names the rule or field. Decide whether the flow should supply better data or the rule should exempt this case.
What does CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY mean in a flow error email?
An Apex trigger on the object your flow wrote to threw an exception. The flow is named because it owned the transaction, but the bug is in the trigger. The email usually appends the trigger's own message after the error code. In a debug log, read forward from FLOW_ELEMENT_BEGIN to the trigger's CODE_UNIT_STARTED: the EXCEPTION_THROWN inside it is the real cause.
Trace the failure in one click
Reproduce the error with ForceLens Smart Capture and the analyzed log opens itself: flow events, triggers, validation and limits, all in order. Free and local.