
Picture this: you've built a Canvas App that your sales team uses to log customer visits. The app connects to SharePoint, pulls in account data, and lets reps submit notes on the go. It works beautifully in testing. Then, two weeks after launch, a rep is standing in a client's lobby with spotty Wi-Fi, tries to submit a form, and the app freezes. No message. No explanation. Just a spinner that eventually gives up. The rep doesn't know if their data was saved. They submit again. Now there are duplicates. Your manager gets a call.
This is not a data problem. This is an error handling problem — and it's one of the most commonly neglected areas when building Canvas Apps. Most tutorials show you how to connect a form to a data source and submit it. Almost none of them show you what to do when that connection fails, when the data source returns an unexpected value, or when a formula blows up quietly in the background without telling anyone. The result is apps that work great in demos and break in production.
By the end of this lesson, you'll know how to build Canvas Apps that handle failure gracefully — apps that communicate clearly with users, recover from errors where possible, and fail loudly and informatively when they can't. You'll understand the error handling toolkit Power Apps gives you, why the default behavior is almost never what you want, and how to implement patterns that hold up in real-world conditions.
What you'll learn:
IfError, IsError, and Error functions to intercept and respond to failuresNotify that are actually usefulYou should already be comfortable building Canvas Apps — connecting to data sources like SharePoint or Dataverse, using galleries and forms, and writing intermediate Power Apps formulas. You don't need to be an expert, but terms like Patch, Filter, LookUp, and SubmitForm should feel familiar. If you've ever asked "why didn't that formula work?" and had trouble answering that question, this lesson is exactly what you need next.
Before we get into syntax, let's understand the problem we're solving.
Power Apps uses a concept called error propagation. When a formula produces an error — say, a LookUp returns blank because the record doesn't exist, or a Patch fails because the connection timed out — Power Apps doesn't crash the app. Instead, it propagates an error value through the formula chain. Controls that depend on that value may display nothing, show a default, or behave unexpectedly.
This is actually a deliberate design choice. Power Apps is meant to be resilient at the formula level. But the side effect is that errors become invisible to users unless you explicitly surface them. The app keeps running. The user has no idea something went wrong.
Here's a concrete example. Suppose you have a LookUp to find a customer record:
// This formula runs when a screen loads
Set(
gblCurrentCustomer,
LookUp(Customers, CustomerID = varSelectedID)
)
If varSelectedID doesn't match any record — maybe it was cleared, maybe the record was deleted — gblCurrentCustomer becomes blank. Every label, form, and gallery that references gblCurrentCustomer silently shows nothing. The user stares at a blank screen, wondering if the app loaded. There's no error. There's just... nothing.
Power Apps does give you a yellow warning triangle icon in the formula bar when it detects formula errors at design time. But runtime errors — the kind that happen because of bad data, network failures, or unexpected user inputs — are a different story entirely. Those require you to handle them explicitly.
Key mindset shift: In Power Apps, silence is not success. A formula completing without crashing does not mean it did what you intended. You have to verify results and communicate them.
IfError is your primary error-handling function. It evaluates an expression and, if that expression produces an error, runs a fallback expression instead. The syntax is:
IfError( Value, Fallback1 [, Value2, Fallback2, ... ] [, DefaultResult] )
Think of it like a try-catch block in traditional programming. The first argument is what you want to do. The second is what to do if it fails. You can chain multiple value/fallback pairs, and add a final default result that runs if everything else fails.
Here's a simple, practical example — loading a customer record safely:
Set(
gblCurrentCustomer,
IfError(
LookUp(Customers, CustomerID = varSelectedID),
Notify(
"Customer record could not be loaded. Please check your connection and try again.",
NotificationType.Error
)
)
)
Now if the LookUp fails — network error, source unavailable, whatever — the user sees a clear notification instead of a blank screen. The variable gblCurrentCustomer will be blank, but at least the user knows something went wrong.
IsError returns true if a value is an error value, false if it isn't. It's useful when you want to check whether something failed and branch your logic accordingly, without immediately running a fallback.
// Check if a data operation returned an error before proceeding
If(
IsError(gblCurrentCustomer),
Navigate(ErrorScreen, ScreenTransition.Fade),
Navigate(CustomerDetailScreen, ScreenTransition.Fade)
)
IsError is particularly valuable when you've stored a result in a variable or collection and want to validate it before using it downstream.
When you're inside an IfError block, you have access to special objects that describe what went wrong:
FirstError — an object representing the first error that occurred. It has properties like Kind, Message, and Source.AllErrors — a table of all errors that occurred (useful when multiple things could fail at once).The FirstError.Message property is especially useful for logging or for surfacing technical details to admins while showing a friendly message to users. Here's what that looks like:
IfError(
Patch(
CustomerVisits,
Defaults(CustomerVisits),
{
CustomerID: gblCurrentCustomer.ID,
VisitDate: DatePicker.SelectedDate,
Notes: NotesInput.Text,
Rep: User().FullName
}
),
// Fallback block — runs only if Patch fails
Notify(
"Visit could not be saved. Error: " & FirstError.Message,
NotificationType.Error
)
)
Warning: Be careful about exposing
FirstError.Messagedirectly to end users in production. The technical message may be confusing or could leak information about your data structure. Use it for logging and show a friendly message to users instead.
The FirstError.Kind property returns an ErrorKind enum value that tells you the category of error. Some useful values:
| ErrorKind | Meaning |
|---|---|
ErrorKind.Network |
Connectivity issue |
ErrorKind.Conflict |
Record was modified by someone else |
ErrorKind.NotFound |
Record doesn't exist |
ErrorKind.Permission |
User lacks access |
ErrorKind.Validation |
Data doesn't meet validation rules |
Knowing the error kind lets you write smarter fallback logic:
IfError(
Patch(CustomerVisits, Defaults(CustomerVisits), varVisitRecord),
Switch(
FirstError.Kind,
ErrorKind.Network,
Notify("No internet connection. Please try again when you're back online.", NotificationType.Warning),
ErrorKind.Permission,
Notify("You don't have permission to save visits. Contact your admin.", NotificationType.Error),
// Default case — catch-all
Notify("Something went wrong. Your visit was not saved.", NotificationType.Error)
)
)
This is the difference between an app that frustrates users and one that actually helps them understand what happened.
The Notify function displays a temporary banner at the top of the screen. It takes a message string and an optional notification type:
Notify( Message, [ NotificationType ] [, Timeout] )
The notification types map to visual treatments:
| Type | Use When |
|---|---|
NotificationType.Success |
An operation completed successfully |
NotificationType.Error |
Something failed and action may be required |
NotificationType.Warning |
Something might be wrong, but the app continues |
NotificationType.Information |
Neutral status update |
The Timeout parameter controls how long the notification stays visible, in milliseconds. The default is 2000ms (2 seconds). For errors, you almost always want a longer timeout — users need time to read and understand the message:
Notify(
"Your visit record was saved successfully.",
NotificationType.Success,
3000
)
Notify(
"Unable to save your visit. Please check your connection and try again.",
NotificationType.Error,
6000
)
This is worth spending a moment on, because most error messages in Canvas Apps are terrible. Avoid vague messages like "An error occurred" or "Something went wrong." Instead, follow this formula:
What happened + Why it happened (if known) + What the user should do next
Compare these:
// Unhelpful
Notify("Error", NotificationType.Error)
// Better
Notify(
"Visit not saved — no internet connection detected. Try again when you're connected.",
NotificationType.Error,
6000
)
// Best (for a conflict error)
Notify(
"This record was updated by someone else while you were editing. Refresh the page to see the latest version.",
NotificationType.Warning,
8000
)
The best error messages respect users' time and intelligence. They don't make users guess what to do.
Form submissions are the most error-prone operation in most Canvas Apps. You're writing data back to a source, which means network calls, validation, permissions checks, and conflict detection all have to work together. Here's how to wrap a full form submission in robust error handling.
When you use SubmitForm, the result is exposed through the form control's OnSuccess and OnFailure properties. This is actually the cleanest pattern for standard forms:
Form's OnSuccess property:
Notify(
"Visit record saved successfully.",
NotificationType.Success,
3000
);
Navigate(CustomerListScreen, ScreenTransition.Fade);
ResetForm(VisitForm)
Form's OnFailure property:
Notify(
"Could not save visit: " & VisitForm.Error,
NotificationType.Error,
6000
)
VisitForm.Error is a built-in property on the form control that contains the error message from the last failed submission. It's not the richest error object, but it's useful.
Many practitioners prefer Patch over SubmitForm because it gives you more control over exactly what gets written. Here's a complete, production-ready pattern for saving a customer visit:
// OnSelect of the "Save Visit" button
// First, validate inputs before even trying to patch
If(
IsBlank(NotesInput.Text),
Notify("Please enter visit notes before saving.", NotificationType.Warning, 4000),
// Inputs are valid — attempt the save
IfError(
// The operation we want to perform
Patch(
CustomerVisits,
Defaults(CustomerVisits),
{
CustomerID: gblCurrentCustomer.ID,
CustomerName: gblCurrentCustomer.Title,
VisitDate: DatePicker.SelectedDate,
Notes: NotesInput.Text,
Status: "Completed",
RepEmail: User().Email,
RepName: User().FullName,
Timestamp: Now()
}
),
// Fallback if Patch fails
Switch(
FirstError.Kind,
ErrorKind.Network,
Notify(
"No connection. Your visit was not saved — please try again once you're online.",
NotificationType.Warning,
8000
),
ErrorKind.Permission,
Notify(
"Permission denied. Contact your administrator to request write access.",
NotificationType.Error,
8000
),
// Default fallback
Notify(
"Visit could not be saved. Please try again or contact support.",
NotificationType.Error,
6000
)
)
)
)
Notice the structure here: we validate first, then attempt the operation, then handle specific failure cases, then fall back to a generic message. This layered approach means you're intercepting the most likely failures with the most helpful messages, while still catching anything unexpected.
Here's where things get genuinely powerful for production apps. Users don't always tell you when something breaks. They just stop using the app, or they work around the problem, or they call your manager. A much better approach: log errors to a data source automatically, so you have visibility into what's failing in production.
Create a SharePoint list (or Dataverse table) called AppErrorLog with these columns:
Title — text (what operation failed)ErrorMessage — text (the technical error message)ErrorKind — number (the ErrorKind enum value)UserEmail — textAppScreen — text (which screen was active)Timestamp — date/timeAppVersion — text (your app version, stored in a global variable)Create a reusable component or named formula called LogError. Since named formulas in Canvas Apps are still somewhat limited, the most practical approach is to put this logic in a helper function pattern using Patch directly:
// This goes inside your IfError fallback block, alongside the Notify
// We run both: tell the user AND log the error
IfError(
Patch(
CustomerVisits,
Defaults(CustomerVisits),
varVisitRecord
),
// Run both actions in sequence using semicolons
Notify(
"Visit could not be saved. Our team has been notified.",
NotificationType.Error,
6000
);
// Log the error silently in the background
Patch(
AppErrorLog,
Defaults(AppErrorLog),
{
Title: "CustomerVisit Patch Failed",
ErrorMessage: FirstError.Message,
ErrorKind: FirstError.Kind,
UserEmail: User().Email,
AppScreen: "VisitEntryScreen",
Timestamp: Now(),
AppVersion: gblAppVersion
}
)
)
Tip: Notice that we don't wrap the logging
Patchin its ownIfError. This is intentional — if error logging fails, we don't want to create an infinite error loop. The user notification already ran, so the critical user-facing work is done. The logging is best-effort.
Now you have a persistent record every time something breaks in production. You can build a simple admin screen in another Canvas App that reads from AppErrorLog, filters by date, and shows you which operations are failing most often. This is the foundation of operational visibility.
One of the most common sources of silent failures in Canvas Apps is the LookUp function returning blank. This happens when:
The naive approach is to just reference the result of LookUp directly. The resilient approach wraps it:
// On screen's OnVisible property
IfError(
Set(
gblCurrentCustomer,
LookUp(Customers, ID = varSelectedCustomerID)
),
// LookUp itself threw an error (permissions, connection, etc.)
Set(gblCurrentCustomer, Blank());
Notify(
"Could not load customer data. Please go back and try again.",
NotificationType.Error,
6000
);
Navigate(CustomerListScreen, ScreenTransition.Fade)
);
// After the IfError, check if the result is blank (record not found)
If(
IsBlank(gblCurrentCustomer),
Notify(
"Customer record not found. It may have been deleted.",
NotificationType.Warning,
5000
);
Navigate(CustomerListScreen, ScreenTransition.Fade)
)
Notice the two-step check: IfError catches formula errors, while the subsequent IsBlank check catches the "no record found" case. These are different failure modes that need separate handling.
One of the most valuable things you can do for mobile users is detect connectivity issues before attempting network operations. Power Apps exposes the Connection.Connected property for exactly this purpose.
Build a reusable pattern by creating a function that checks connectivity before any data operation:
// On the "Save" button's OnSelect property
If(
Not(Connection.Connected),
// No connection — don't even try, give clear guidance
Notify(
"You're offline. Please connect to Wi-Fi or mobile data before saving.",
NotificationType.Warning,
8000
),
// Connected — proceed with the save attempt
IfError(
Patch(CustomerVisits, Defaults(CustomerVisits), varVisitRecord),
Switch(
FirstError.Kind,
ErrorKind.Network,
Notify(
"Connection lost during save. Please check your signal and try again.",
NotificationType.Warning,
6000
),
Notify(
"Save failed unexpectedly. Please try again.",
NotificationType.Error,
6000
)
)
)
)
The pre-flight connectivity check is better UX than letting the operation time out. Users get instant feedback instead of waiting 30 seconds for a timeout error.
Warning:
Connection.Connectedtells you whether the device has a network connection, not whether your specific data source is reachable. A user on a corporate network with SharePoint blocked by firewall will show as connected but still get network errors. Handle both cases.
Let's put everything together in a realistic project. You'll build a customer visit logging screen that handles every major failure mode.
Setup: Create a SharePoint list called CustomerVisits with these columns: Title (text), CustomerID (number), VisitDate (date), Notes (multi-line text), RepEmail (text), Status (choice: Draft, Completed), Timestamp (date/time).
Create a second list called AppErrorLog with the columns described earlier.
In the App's OnStart property:
Set(gblAppVersion, "1.0.3");
Set(gblIsSubmitting, false)
Add a date picker named DatePicker, a text input named NotesInput, and a button named btnSaveVisit. Add a label named lblSubmitStatus with its Visible property set to gblIsSubmitting.
Set lblSubmitStatus.Text to "Saving your visit..." — this gives users feedback during the async operation.
Set btnSaveVisit.OnSelect to:
// Step 1: Validate
If(
IsBlank(NotesInput.Text),
Notify("Visit notes are required.", NotificationType.Warning, 4000),
// Step 2: Check connectivity
If(
Not(Connection.Connected),
Notify(
"You're offline. Connect to a network and try again.",
NotificationType.Warning,
6000
),
// Step 3: Show submitting state
Set(gblIsSubmitting, true);
// Step 4: Attempt the patch with full error handling
IfError(
Patch(
CustomerVisits,
Defaults(CustomerVisits),
{
Title: "Visit - " & User().FullName & " - " & Text(Today(), "[$-en-US]mm/dd/yyyy"),
VisitDate: DatePicker.SelectedDate,
Notes: NotesInput.Text,
RepEmail: User().Email,
Status: { Value: "Completed" },
Timestamp: Now()
}
),
// Fallback: handle the error
Switch(
FirstError.Kind,
ErrorKind.Network,
Notify(
"Connection lost. Your visit was NOT saved. Please try again.",
NotificationType.Warning,
8000
),
ErrorKind.Permission,
Notify(
"You don't have permission to save visits. Contact support.",
NotificationType.Error,
8000
),
Notify(
"Unexpected error. Your visit may not have saved.",
NotificationType.Error,
6000
)
);
// Log the error
Patch(
AppErrorLog,
Defaults(AppErrorLog),
{
Title: "CustomerVisit Save Failed",
ErrorMessage: FirstError.Message,
ErrorKind: FirstError.Kind,
UserEmail: User().Email,
AppScreen: "VisitEntryScreen",
Timestamp: Now(),
AppVersion: gblAppVersion
}
)
);
// Step 5: Hide submitting state (runs whether success or failure)
Set(gblIsSubmitting, false);
// Step 6: Success path — only navigate if no error occurred
// Check this by trying to read from the list
If(
IsBlank(gblLastError),
Notify("Visit saved successfully!", NotificationType.Success, 3000);
Reset(NotesInput)
)
)
)
Tip: The pattern of using a
gblIsSubmittingboolean to show/hide a loading indicator is essential for any operation that takes more than a second. Without it, users tap the button again, causing duplicate submissions.
Set btnSaveVisit.DisplayMode to:
If(gblIsSubmitting, DisplayMode.Disabled, DisplayMode.Edit)
This prevents double-submissions while the Patch is in progress.
This is subtle. IfError only catches errors in the first argument. If your error is in the fallback expression, it propagates normally.
// Wrong — if Notify itself fails (unlikely but possible), it's uncaught
IfError(
Patch(...),
Notify(SomeFunctionThatErrors())
)
// Right — keep your fallback simple and safe
IfError(
Patch(...),
Notify("Save failed. Please try again.", NotificationType.Error, 5000)
)
IsBlank and IsError are different checks. A LookUp that finds no records returns blank, not an error. A LookUp that fails due to a network problem returns an error. You often need both checks in sequence, as shown in the LookUp section above.
If you set gblIsSubmitting = true before an operation and the operation fails inside IfError, the code after the IfError block still runs — but developers sometimes only reset the submitting state in the success path. Make sure Set(gblIsSubmitting, false) runs outside the IfError block, so it always executes regardless of outcome.
Notify banners disappear. If a user is looking away or the notification timeout is too short, they miss it. For critical errors — especially data loss scenarios — consider also showing a visible error label on the form that persists until the user successfully saves or explicitly dismisses it.
// In your IfError fallback
Set(gblSaveError, "Visit could not be saved. Please try again.");
// A visible label on the screen
// Text: gblSaveError
// Visible: Not(IsBlank(gblSaveError))
// Color: Red
Reset gblSaveError to blank on successful save. Now the error persists visually until it's resolved.
If you're using SubmitForm and the form's OnFailure property just shows a generic message, you're throwing away valuable diagnostic information. Always capture YourForm.Error in your OnFailure handler.
If your error handling fallback never seems to run even when you expect errors, check:
ForAll loops behave differently — use IfError inside the ForAll body.Error handling is not a polish-phase concern — it's a fundamental design decision that should be woven into your app from the start. Here's what you've built understanding of in this lesson:
IfError lets you intercept formula errors and run fallback logic, keeping your app responsive and communicative even when operations fail.FirstError.Kind and FirstError.Message give you the information you need to write specific, actionable error responses instead of generic catch-all messages.Notify is your primary user communication tool, but it works best when paired with persistent UI indicators for critical errors.gblIsSubmitting + DisplayMode.Disabled) prevents duplicate submissions and gives users clear feedback during async operations.Where to go from here:
The natural next step is understanding offline capabilities in Canvas Apps — if you're building for mobile users or field workers, you'll want to explore SaveData and LoadData for local storage, which lets users capture data even without connectivity and sync it later. Pair that with the error handling patterns in this lesson, and you'll be able to build apps that work reliably under nearly any conditions.
You should also explore Power Apps Monitor more deeply — it's a professional debugging tool that shows you real-time formula execution traces, network calls, and error details. Knowing how to read a Monitor trace will dramatically reduce the time you spend chasing down runtime bugs.
Finally, consider building a dedicated Error Screen in your apps — a screen you can navigate to when something catastrophic happens (like failing to load the data the entire app depends on), with clear messaging, a "Try Again" button, and contact information for support. It's a small investment that makes a big difference in how users perceive app quality.
Learning Path: Canvas Apps 101