Power Apps Error Handling

Catch failures, show useful messages, and handle concurrency. IfError, IsError, Errors(), form OnFailure, and the SharePoint unique-key race condition.

Last verified July 2026 against current Power Fx.

Quick answer

  • Confirm formula-level error management is on, or none of this works
  • Branch on FirstError.Kind, never on the message text
  • Capture the result of every Patch and test it with IsError
  • Form controls use OnSuccess and OnFailure, not IfError
  • Unique-value collisions need a retry, validation cannot prevent them

The difference between a flaky app and a solid one is usually error handling. A Patch to SharePoint can fail for a dozen reasons. A lookup misses. A conversion throws. A required field is blank. Another user claims the same unique value half a second before you do. By default those failures surface as ugly banners or silent blanks.

This page covers the patterns that keep it under control, ending with the one most guides skip: what to do when two people save at the same time.

Before you start: turn the feature on

Formula-level error management is on by default for new apps, but older apps may have it disabled. If it is off, IfError and IsError will not behave the way anything on this page describes, and errors come back as blank values instead.

Check it at Settings > Updates > Retired, and make sure "Disable formula-level error management" is off.

How errors travel

Errors propagate through formulas the way #DIV/0! propagates through a spreadsheet. An error input produces an error output, all the way up the chain.

An error passes straight through
Mid(Text(1 / 0), 1, 1) // the division error passes through Text() and Mid()

Three functions are designed to intercept an error instead of passing it along: IfError, IsError, and IsBlankOrError. Everything else forwards it. You are not catching exceptions at a call site, you are putting a filter somewhere in a pipe.

The four building blocks

IsError: did it fail?

IsError(Value(txtQty.Text)) // true if the text is not a number

IfError: recover with a fallback

IfError( LookUp(Items, ID = varId), { Title: "Not found" } // returned if the LookUp errors)

IsBlankOrError: the usual real-world check

// Blank and error are different things, and you almost always// want to treat them the same way in a UI.If(IsBlankOrError(varRate), 0, varRate)

Errors(): per-field messages after a failed write

// After a failed Patch or SubmitForm on a data source:Concat(Errors(YourList), Message, Char(10))

Use FirstError.Message when you want one line for a notification. Use Errors(DataSource) when several fields failed at once and the user needs to see all of them.

Watch the argument count on IfError

This trips up nearly everyone. The signature is:

IfError(Value1, Fallback1, Value2, Fallback2, ..., DefaultResult)

IfError is not if-then-else

It tests each Value in order and returns the matching Fallback for the first one that errors. It is not IfError(action, condition, thenDo, elseDo). If you write it that way, your second argument gets evaluated as a replacement value and discarded, and your third argument runs on the success path.
Wrong vs right
// WRONG: this is four arguments, not a condition and two branches.// The retry block runs when SubmitForm SUCCEEDS.IfError( SubmitForm(frmCase), "key" in FirstError.Message, // treated as Fallback1, evaluated and thrown away /* retry block */, // treated as Value2, runs on success false) // RIGHT: branch inside the fallback.IfError( SubmitForm(frmCase), If( FirstError.Kind = ErrorKind.ConstraintViolated, /* retry */, Notify("Save failed: " & FirstError.Message, NotificationType.Error) ))

Patch with an error check

Capture the result of a Patch and branch on it. This is the single most valuable pattern on this page.

Safe create
Set(gblResult, Patch( YourSharePointList, Defaults(YourSharePointList), { Title: txtTitle.Text, Status: { Value: "Active" }, AssignedTo: cmbAssignee.Selected } ));If( IsError(gblResult), Notify("Save failed: " & FirstError.Message, NotificationType.Error), Notify("Saved successfully", NotificationType.Success); Navigate(scrList, ScreenTransition.None))

Note the property names. Classic and modern text inputs both expose .Text, not .Value. A ComboBox exposes .Selected for a single record and .SelectedItems for multi-select. Getting this wrong is the most common reason a Patch fails with a type error rather than a data error.

Validate before you write

Cheaper than handling a failure is preventing it. Short-circuit on invalid input with a chained If before the Patch ever runs.

Guard clauses, then save
If( IsBlank(txtTitle.Text), Notify("Title is required", NotificationType.Warning),  IsBlank(cmbStatus.Selected), Notify("Select a status", NotificationType.Warning),  // all valid, save Set(gblResult, Patch(YourSharePointList, Defaults(YourSharePointList), { Title: txtTitle.Text, Status: cmbStatus.Selected } ) ); If( IsError(gblResult), Notify("Save failed: " & FirstError.Message, NotificationType.Error), Notify("Saved", NotificationType.Success) ))

Validation prevents mistakes the user can fix. It cannot prevent failures that depend on what other users are doing at the same moment. That is the next section.

Forms: OnSuccess and OnFailure

If you use a Form control with SubmitForm, do not wrap the submit in IfError for ordinary error handling. The form has dedicated behavior properties.

frmItem.OnSuccess
Notify("Saved", NotificationType.Success);Back()
frmItem.OnFailure
Notify("Could not save: " & frmItem.Error, NotificationType.Error)

Gate the submit on frmItem.Valid so required-field errors show inline before you attempt the write.

btnSubmit.OnSelect
If( frmItem.Valid, SubmitForm(frmItem), Notify("Fix the highlighted fields", NotificationType.Warning))

The form also exposes frmItem.ErrorKind, which gives you the same enum value as FirstError.Kind. Use it in OnFailure when you want to react differently to a permission failure than to a conflict. The one time you do wrap SubmitForm in IfError is when you intend to retry the submit yourself. That is the concurrency case below.

Concurrency: when two users save at once

This is the failure mode that validation cannot catch and that most tutorials never mention.

The scenario

You have a SharePoint list with a column that must be unique. Maybe it is a case key like FY26-1043, built from a fiscal year plus an incrementing number. You turned on Enforce unique values on the column, which is the right call.

Your app computes the next number by reading the highest existing one and adding 1. Two users open the form within a few seconds of each other. Both read 1042. Both compute 1043. The first save wins. The second one gets rejected by SharePoint with a constraint violation, and unless you handle it, the user sees a generic red banner and loses their work.

Why the constraint is doing the real work

It is worth being clear about what is protecting your data here. It is not your formula. Any client-side "read the max, add one" is racy by construction, because there is a window between the read and the write during which anyone else can write.

The unique constraint on the column is the only thing that guarantees correctness. Your Power Fx is damage control: it catches the rejection, gets a fresh number, and tries again so the user does not have to. Never treat the client-computed key as authoritative.

Getting the next number correctly

Two things go wrong in the version of this most people write.

Both of these are wrongNot delegable
Last(Sort(Filter(Cases, FY.Value = cmbFY.Selected.Value), Created, SortOrder.Ascending)).case_id + 1Max(Filter(Cases, FY.Value = cmbFY.Selected.Value), case_id) + 1

Last() and Max() are not delegable against SharePoint

Last() is not delegable. Past the 500-row limit you are taking the last row of a truncated page, not the last row of the list. Max() is not delegable against SharePoint either, so it silently operates on the first page only. Sorting by Created is also the wrong key. It gives you the case_id of the most recently created row, which is only the highest number if nobody ever deleted a record or created one out of order.

The delegable version sorts on the number itself and takes the first row:

Delegable next-numberDelegable
Coalesce( First( Sort( Filter(Cases, FY.Value = cmbFY.Selected.Value), case_id, SortOrder.Descending ) ).case_id, 0) + 1

Sort and Filter both delegate against SharePoint provided case_id and FY are indexed columns. First then takes the top of the sorted result rather than the top of an arbitrary page. Coalesce handles the first record of a new fiscal year, where the filter returns nothing. Index both columns in list settings. Without the index, SharePoint will refuse to sort past 5,000 items and you are back to a truncated page.

The gotcha that breaks the naive retry

Here is the trap. A common retry looks like this:

Naive retry, can resubmit the OLD key
IfError( SubmitForm(frmCase), Refresh(Cases); Set(gblNextCaseId, /* recomputed max + 1 */); SubmitForm(frmCase) // this may submit the OLD key)

Set() does not reach control properties in the same formula

Set() is not synchronous with respect to downstream control properties. The key that gets written comes from a data card whose Update reads a control that reads gblNextCaseId, and the control tree does not necessarily recalculate before the second SubmitForm runs in the same behavior chain. The same reasoning kills the loading spinner: a locLoading toggled true then false in one formula never renders, because the screen does not repaint mid-behavior-formula.

There are two ways around it.

Option A: Patch directly, skip the form for the key

The cleanest fix is to not depend on a control's value at all. Compute the key inside the formula, so the retry uses a value you control directly.

btnSubmit.OnSelect
With( { wNextId: Coalesce( First(Sort(Filter(Cases, FY.Value = cmbFY.Selected.Value), case_id, SortOrder.Descending)).case_id, 0 ) + 1 }, IfError( Patch(Cases, Defaults(Cases), { Title: txtTitle.Text, FY: cmbFY.Selected, case_id: wNextId, test_key: cmbFY.Selected.Value & "-" & wNextId } ),  // first attempt failed, decide whether it is worth retrying If( FirstError.Kind = ErrorKind.ConstraintViolated,  Refresh(Cases); With( { wRetryId: Coalesce( First(Sort(Filter(Cases, FY.Value = cmbFY.Selected.Value), case_id, SortOrder.Descending)).case_id, 0 ) + 1 }, IfError( Patch(Cases, Defaults(Cases), { Title: txtTitle.Text, FY: cmbFY.Selected, case_id: wRetryId, test_key: cmbFY.Selected.Value & "-" & wRetryId } ), Notify( "That key was claimed twice while saving. Please try once more.", NotificationType.Error ), Notify("Saved as " & cmbFY.Selected.Value & "-" & wRetryId, NotificationType.Success); Back() ) ),  Notify("Save failed: " & FirstError.Message, NotificationType.Error) ),  // first attempt succeeded Notify("Saved as " & cmbFY.Selected.Value & "-" & wNextId, NotificationType.Success); Back() ))

The nested With matters. A With binding is evaluated once, so reusing wNextId after the Refresh would give you the stale number. The inner With re-reads after the refresh. One retry is the right number: if two collisions happen back to back you have a load problem, not a race, and hammering SharePoint will make it worse.

Option B: keep the form, retry on a timer

If you need the form control for its validation and card layout, break the retry across a repaint using a timer. The timer tick gives the control tree a chance to pick up the new variable before the second submit.

btnSubmit.OnSelect
If( !frmCase.Valid, Notify("Fix the highlighted fields", NotificationType.Warning), SubmitForm(frmCase))
frmCase.OnFailure
If( frmCase.ErrorKind = ErrorKind.ConstraintViolated && !locRetrying,  Refresh(Cases); Set(gblNextCaseId, Coalesce( First(Sort(Filter(Cases, FY.Value = cmbFY.Selected.Value), case_id, SortOrder.Descending)).case_id, 0 ) + 1 ); UpdateContext({ locRetrying: true }),  Notify("Could not save: " & frmCase.Error, NotificationType.Error); UpdateContext({ locRetrying: false }))
tmrRetry
// AutoStart: false// Repeat: false// Duration: 400// Start: locRetrying// Reset: !locRetrying// OnTimerEnd:UpdateContext({ locRetrying: false });SubmitForm(frmCase)

The locRetrying guard is what stops an infinite loop. Without it, a second failure re-enters OnFailure, restarts the timer, and you have built a retry storm. Because the timer does span a repaint, this is also the version where a loading overlay actually works. Bind the overlay's Visible to locRetrying.

Confirm the ErrorKind in your own tenant

ConstraintViolated (8) is what SharePoint returns for a unique-column rejection in most cases, but the connector can surface a conflict as Conflict (6) or Validation (11) depending on the failure path and the column type. Do not guess. Drop a temporary label on the screen and set its Text to frmCase.ErrorKind, force a duplicate by hand, and read what you actually get. It takes two minutes and saves an afternoon.

When to stop doing this in the app

If the key genuinely matters (it appears on a document or gets quoted to a customer), move the generation server-side. A Power Automate flow triggered by the app is a single writer: it reads the max, writes the record, and returns the key, with no window for another client to slip in. Simplest of all: if you do not truly need a formatted key, SharePoint's built-in ID column is already unique, already generated server-side, and already free.

The app-side retry is the right answer when you want a readable key, low volume, and no extra infrastructure. It is the wrong answer for high-concurrency or compliance-critical numbering.

Coalesce for safe defaults

Many "errors" are really just blanks propagating. Coalesce returns the first non-blank value, which kills a whole class of downstream failures.

Coalesce(varUser.FullName, "Unknown user")Coalesce(ThisItem.Amount, 0) * 1.08Coalesce(Param("id"), "new")

Note that Coalesce handles blank, not error. For an expression that can throw, wrap it: IfError(Value(txt.Text), 0).

App.OnError: the global net

App.OnError runs for every unhandled error in the app. It cannot fix or replace the error, because by the time it runs the error has already propagated through your formulas. What it can do is log and control how the failure is reported.

App.OnError
Trace($"Error: {FirstError.Message} | Source: {FirstError.Source}");Error(FirstError) // rethrow so the user still sees the banner

Leaving out the Error(FirstError) line suppresses the banner entirely, which is occasionally what you want and usually is not. Silent failure is worse than an ugly banner. To suppress one specific noisy kind and keep everything else:

If(FirstError.Kind <> ErrorKind.Div0, Error(FirstError))

Custom errors

You can raise your own errors and catch them like any other.

// RaiseError({ Kind: ErrorKind.Custom, Message: "This report was already submitted for that date" }) // Or with your own numeric kind, so you can branch on it laterError({ Kind: 1001, Message: "Case is locked for editing" })

Use values above 1000 for custom kinds so you never collide with a system value Microsoft adds later.

Errors inside ForAll

ForAll does not stop at the first error. Every iteration runs and the errors accumulate, which is usually what you want for a bulk operation but surprises people the first time.

Clear(colFailed);IfError( ForAll(colChanges As c, Patch(YourList, LookUp(YourList, ID = c.ID), { Status: c.Status }) ), ForAll(AllErrors As e, Collect(colFailed, { Message: e.Message })));If( !IsEmpty(colFailed), Notify(CountRows(colFailed) & " of " & CountRows(colChanges) & " rows failed to save", NotificationType.Error))

AllErrors is the table form of FirstError and is available in the same scopes.

ErrorKind reference

Every value in the enum, with what actually triggers it in practice.

ErrorKindValueTypically triggered by
None0No error
Sync1Data source rejected the operation; read the Message for the real cause
MissingRequired2A required column was omitted or blank in a Patch
CreatePermission3User lacks Add permission on the list or table
EditPermissions4User lacks Edit permission, or the item is checked out to someone else
DeletePermissions5User lacks Delete permission
Conflict6The record changed at the source since you read it (optimistic-concurrency conflict)
NotFound7LookUp or Patch targeted a record that no longer exists
ConstraintViolated8Server-side constraint failed, including a SharePoint Enforce-unique-values column
GeneratedValue9You passed a value for a calculated or auto-generated column
ReadOnlyValue10You wrote to a read-only column (common with SharePoint system columns)
Validation11Column or list validation formula rejected the value
Unknown12Connector returned something Power Fx could not classify
Div013Division by zero
BadLanguageCode14Invalid locale tag in Text or Value
BadRegex15Malformed pattern in IsMatch, Match, or MatchAll
InvalidFunctionUsage16Wrong argument count or shape
FileNotFound17LoadData found no saved file
AnalysisError18Compiler could not analyze the formula
ReadPermission19User lacks read access to the source
NotSupported20Operation unavailable on this device or player
InsufficientMemory21Device is out of memory or storage
QuoteExceeded22Storage quota exceeded
Network23Request failed in transit, often intermittent, worth retrying
Numeric24Numeric function given an out-of-domain value, e.g. Sqrt(-1)
InvalidArgument25Argument of the wrong type
Internal26Internal platform error
NotApplicable27Deliberate blank that should be surfaced rather than ignored
Customn/aRaised by your own Error() call

Kinds worth branching on specifically: 6, 8, and 11 for write conflicts and constraints; 3, 4, 5, and 19 for permissions; and 23 for transient network failures.

Rules of thumb

  • Wrap anything that can throw: Patch, Value(), ParseJSON, and LookUp on a maybe-missing row are the usual suspects.
  • Confirm formula-level error management is on before you rely on any of this.
  • Branch on FirstError.Kind, not on FirstError.Message. Message text is locale dependent and Microsoft rewrites it without warning.
  • Show a message the user can act on. Include the underlying message rather than a bare "Error."
  • Prefer a validation guard over a recovery path. An error you prevent never needs a message.
  • Never assume a client-computed unique value is safe. Enforce it at the data source and treat your formula as recovery.
  • Retry once, not in a loop, and always with a guard variable.
  • Remember that Set() does not propagate to control properties within the same behavior formula. Break the chain with a timer or bypass the control entirely.
  • Remember that a loading indicator toggled and untoggled inside one behavior formula never renders.

FAQ

What is the difference between IsError, IfError and Errors()?

IsError(x) returns true or false depending on whether an expression errored. IfError(x, fallback) returns x, or the fallback if x errored, and it is the one you use to recover. Errors(DataSource) returns a table of the most recent errors for a data source, which is how you surface several per-field validation messages after one failed write.

How do I know if a Patch succeeded in Power Apps?

Capture the result and test it: Set(gblResult, Patch(...)); If(IsError(gblResult), Notify(FirstError.Message, NotificationType.Error), Notify("Saved")). Patch returns the saved record on success and an error value on failure.

Why does my app crash instead of showing an error message?

Unhandled errors bubble up as banners or blank values. Wrap risky operations in IfError and confirm that formula-level error management is enabled in Settings, because with it off the functions on this page behave differently.

Why does my Power Apps form fail when two users submit at the same time?

Because a client-computed unique value is read and written in two separate steps, and another user can write in between. The second submit is rejected by the data source with ErrorKind.ConstraintViolated. Handle it by catching the error, refreshing the source, recomputing the value, and submitting once more.

How do I handle a SharePoint Enforce unique values error in Power Apps?

Catch it in IfError or the form's OnFailure, check for ErrorKind.ConstraintViolated, call Refresh on the list, recompute the key from the refreshed data, and retry a single time behind a guard variable. Confirm the exact ErrorKind in your own environment first, since the connector can surface it as Conflict or Validation depending on the column.

How do I generate a unique ID in Power Apps without duplicates?

You cannot guarantee it from the client. Enforce uniqueness on the column so the data source rejects collisions, then add a retry in the app for a better user experience. For anything where the number must never collide, generate it in a Power Automate flow, which is a single writer, or use SharePoint's built-in ID column.

Why is my max plus one returning the wrong number?

Usually delegation. Last() and Max() are not delegable against SharePoint, so they operate on the first page of results rather than the whole list. Use First(Sort(Filter(...), yourColumn, SortOrder.Descending)).yourColumn and index the columns you sort and filter on.

Why does my retry submit the same value that just failed?

Because Set() does not propagate to control properties inside the same behavior formula. If the value being submitted comes from a data card reading a variable, the card has not recalculated yet when the second submit runs. Either compute the value inline and Patch directly, or break the chain with a timer.