Build production-grade error handling in Microsoft Fabric pipelines that actually catches failures, routes them intelligently, and sends actionable email alerts — across Copy, Dataflow Gen2, and Notebook activities. Learn the dependency condition model, activity output expressions, and centralized error handler patterns that keep your medallion pipelines resilient.

Picture this: it's Monday morning, your sales leadership is waiting for the weekly revenue dashboard to reflect the weekend's transactions, and your pipeline silently failed at 2 AM because the source REST API returned a 503. Nobody got an alert. Nobody knows. Power BI is showing stale data from Friday. The pipeline logs are sitting in the Monitoring Hub waiting to be discovered, but nobody thought to check them because the assumption was everything worked fine.
This is the scenario that end-to-end error handling exists to prevent. A pipeline that runs unattended without proper failure routing is not a production pipeline — it's a time bomb. In Microsoft Fabric, you have all the primitives you need to build genuinely resilient orchestration: dependency conditions that route execution based on upstream outcomes, If Condition activities that evaluate dynamic expressions, Office 365 Outlook activities that fire email alerts, and a rich @activity() expression language that lets you interrogate exactly what failed and why. The challenge is knowing how these pieces fit together across the heterogeneous mix of Copy, Dataflow Gen2, and Notebook activities that make up a real-world Fabric pipeline.
By the end of this lesson, you will know how to architect error handling that actually works in production — not just the happy path. You'll understand how dependency conditions chain, why Dataflow Gen2 activities require special handling, how to extract failure metadata from the @activity() object and embed it in alert emails, and how to use If Condition activities to branch execution based on business logic rather than just technical failures.
What you'll learn:
@activity() expressions to evaluate outcomes and route execution dynamicallyThis lesson assumes you are comfortable with the following:
Before you can handle failures, you need to understand how Fabric pipelines model execution flow. Every arrow you draw between two activities in the pipeline canvas is not just a sequence — it's a dependency with a condition. That condition determines under which outcome of the upstream activity the downstream activity is allowed to run.
Fabric exposes four dependency conditions:
Success — the downstream activity runs only if the upstream activity completed without error. This is the default when you draw a connection between activities.
Failure — the downstream activity runs only if the upstream activity failed. This is your primary tool for error routing.
Completion — the downstream activity runs regardless of whether the upstream activity succeeded or failed. This is useful for cleanup operations, audit logging, or setting status flags that need to happen no matter what.
Skipped — the downstream activity runs if the upstream activity was skipped due to an If Condition branch that wasn't taken, or if the upstream activity itself was dependent on a Skipped predecessor.
Here is the critical subtlety that trips people up: an activity can have multiple incoming dependency conditions simultaneously. If Activity B depends on Activity A with both a Success and a Failure condition, then B runs after A regardless of A's outcome — equivalent to Completion. This matters when you're building centralized error handlers that need to catch failures from multiple upstream activities.
Warning
The Completion condition and the combination of Success + Failure conditions are not identical in all scenarios. Completion triggers even if an activity is explicitly cancelled. Success + Failure will not run if the activity was cancelled. For monitoring pipelines that run unattended overnight, this distinction matters — a cancellation mid-run should behave differently than a clean failure.
Another important nuance: dependency conditions are evaluated per activity pair, not per pipeline run globally. If you have Activity A → Activity B (Success) → Activity C (Success), and Activity A fails, then B is never triggered, and C's dependency on B is evaluated against B's status — which is "Skipped," not "Failed." If C only has a Success dependency on B, C also gets Skipped. This cascading Skipped behavior is the most common source of error handling bugs in Fabric pipelines.
Before we start wiring up failure paths, you need to understand what information is available to you when an activity fails. The @activity() expression function is how you access it.
Every completed activity — regardless of success or failure — exposes a rich output object. The general structure is:
{
"status": "Failed",
"output": {
"errors": [
{
"Code": "UserErrorInvalidParameter",
"Message": "The connection to the source could not be established.",
"Details": "..."
}
]
},
"error": {
"errorCode": "UserErrorInvalidParameter",
"message": "The connection to the source could not be established.",
"failureType": "UserError",
"target": "Copy_SalesTransactions"
}
}
In pipeline expressions, you access this with:
@activity('Copy_SalesTransactions').output.errors[0].Message
@activity('Copy_SalesTransactions').error.message
@activity('Copy_SalesTransactions').error.errorCode
@activity('Copy_SalesTransactions').error.failureType
@activity('Copy_SalesTransactions').status
The failureType property is particularly valuable for routing logic. Fabric distinguishes between:
UserError — a configuration or data issue (wrong connection string, schema mismatch, invalid parameter). Retrying will not fix this.SystemError — a transient infrastructure problem (service unavailable, timeout). Retrying might fix this.This distinction lets you build smarter error handling: for SystemError, you might trigger a retry loop or a compensating action; for UserError, you escalate immediately to a human.
Tip
The @activity() function only works in activities that depend on the target activity, directly or transitively. You cannot call @activity('CopyActivity1') from an activity that has no dependency path back to CopyActivity1. Plan your graph topology with this in mind.
For Notebook activities specifically, you have an additional escape hatch. Within the notebook itself, you can set the exit value:
# In your PySpark notebook
mssparkutils.notebook.exit("VALIDATION_FAILED: Row count is 0 for sales_transactions_2024_01")
This exit value surfaces in the pipeline as @activity('Transform_SilverLayer').output.result.exitValue. You can evaluate it in a downstream If Condition activity to make decisions based on semantic business outcomes, not just technical pass/fail status.
Theory is useful, but what does a well-architected error handling pattern actually look like in a real Fabric pipeline? Let's build one based on a concrete scenario.
The scenario: You're running a nightly medallion pipeline that:
You want failures at any stage to: (a) stop downstream processing immediately, (b) capture the specific error details, (c) email the data engineering team with actionable information, and (d) ideally distinguish between retriable infrastructure failures and non-retriable configuration failures.
Here's the high-level graph topology you're aiming for:
[Copy_Bronze] ──(Success)──► [Dataflow_Silver] ──(Success)──► [Notebook_Gold] ──(Success)──► [Email_Success]
│ │ │
└──(Failure)──► └──(Failure)──► └──(Failure)──►
│ │ │
└──────────────────────────────┴──────────────────────────────┘
│
[If_IsRetriable]
/ \
(True branch) (False branch)
[Email_RetryAlert] [Email_EscalationAlert]
The key design decision here is the centralized error handler pattern: all three failure paths converge on a single If_IsRetriable activity, rather than each activity having its own disconnected error handler. This keeps the pipeline graph readable and ensures consistent error handling behavior regardless of which stage fails.
Key insight
The centralized error handler pattern has one important limitation: because @activity() expressions require a direct dependency path, you can't call @activity('Copy_Bronze') from within the If Condition if the If Condition was triggered by a Dataflow failure. The solution is to use pipeline variables as error capture points — each failure path sets pipeline variables before converging on the handler.
Since we can't interrogate all upstream activities from a single centralized handler, we use pipeline variables as the intermediary. Add the following variables to your pipeline (Pipeline settings → Variables):
| Variable Name | Type | Default Value |
|---|---|---|
ErrorMessage |
String | (empty) |
ErrorCode |
String | (empty) |
ErrorSource |
String | (empty) |
FailureType |
String | (empty) |
PipelineRunId |
String | (empty) |
Each failure path gets a Set Variable activity that captures the relevant information before passing control to the centralized handler.
For the Copy activity failure path, add a Set Variable activity named SetError_Copy with these assignments:
ErrorMessage:
@concat('Copy activity failed: ', activity('Copy_Bronze').error.message)
ErrorCode:
@activity('Copy_Bronze').error.errorCode
ErrorSource:
Copy_Bronze
FailureType:
@activity('Copy_Bronze').error.failureType
PipelineRunId:
@pipeline().RunId
For the Dataflow Gen2 failure path, the expression structure is similar but there's an important difference: Dataflow Gen2 activities surface their errors differently.
Dataflow Gen2 activities are notoriously less transparent in their error reporting than Copy activities. When a Dataflow Gen2 fails, the error structure often looks like this:
{
"error": {
"errorCode": "DFExecutionFailed",
"message": "Dataflow refresh failed.",
"failureType": "UserError"
}
}
That message — "Dataflow refresh failed" — is spectacularly unhelpful. The actual root cause is buried in the Dataflow refresh history within the Fabric portal, not surfaced cleanly in the pipeline activity output.
This has two practical implications for your error handling design:
First, your error message for Dataflow failures should direct the on-call engineer to look in the right place:
@concat(
'Dataflow Gen2 [Silver_Dedup] failed at ',
formatDateTime(utcNow(), 'yyyy-MM-dd HH:mm:ss'),
' UTC. Pipeline Run ID: ',
pipeline().RunId,
'. Review the Dataflow refresh history in the Fabric portal for root cause details.'
)
Second, because Dataflow errors are almost always classified as UserError by the pipeline engine regardless of whether the actual cause is transient or not, you should implement a more conservative retry policy for Dataflows at the activity level (via the Settings tab on the Dataflow activity) rather than relying on your FailureType routing logic.
Warning
The failureType field for Dataflow Gen2 activities is unreliable as a routing signal for retriable vs. non-retriable errors. A transient Power Query engine timeout will appear as UserError: DFExecutionFailed exactly the same as a genuine M query syntax error. Always configure activity-level retries on Dataflow Gen2 activities (2-3 retries with a 30-second wait) and treat all Dataflow failures that reach your pipeline-level error handler as requiring human investigation.
For the Dataflow failure path, your Set Variable activity looks like:
ErrorMessage:
@concat(
'Dataflow Gen2 [Silver_Dedup] failed. ',
'Error: ', activity('Dataflow_Silver').error.message,
' | Run ID: ', pipeline().RunId,
' | Check Fabric portal > Dataflow refresh history for details.'
)
FailureType:
@if(
equals(activity('Dataflow_Silver').error.errorCode, 'DFExecutionFailed'),
'RequiresInvestigation',
activity('Dataflow_Silver').error.failureType
)
Notice how we're overriding the FailureType for the DFExecutionFailed case with a custom value RequiresInvestigation — this tells our downstream If Condition that Dataflow failures always need a human, bypassing the auto-retry routing.
Notebooks are the richest source of failure information, but they require you to write that information yourself. A notebook that crashes with an unhandled exception surfaces in the pipeline with a generic error. A notebook that uses mssparkutils.notebook.exit() strategically can communicate semantic failure reasons.
Here's a pattern for a Gold layer aggregation notebook that distinguishes between different failure modes:
from pyspark.sql import SparkSession
import traceback
spark = SparkSession.builder.getActiveSession()
# Read from Silver layer
try:
df_silver = spark.read.format("delta").load(
"abfss://sales-lakehouse@onelake.dfs.fabric.microsoft.com/Silver/sales_transactions"
)
except Exception as e:
mssparkutils.notebook.exit(f"TECHNICAL_ERROR: Could not read Silver table. {str(e)}")
# Business validation
row_count = df_silver.count()
if row_count == 0:
mssparkutils.notebook.exit(
"BUSINESS_ERROR: Silver table is empty. Possible upstream Dataflow failure. "
f"Expected >0 rows for date partition {processing_date}."
)
# Run aggregations
try:
df_gold = df_silver.groupBy("region", "product_category", "sale_date") \
.agg(
{"revenue": "sum", "transaction_id": "count", "quantity": "sum"}
) \
.withColumnRenamed("sum(revenue)", "total_revenue") \
.withColumnRenamed("count(transaction_id)", "transaction_count") \
.withColumnRenamed("sum(quantity)", "total_quantity")
df_gold.write.format("delta").mode("overwrite").option("overwriteSchema", "true").save(
"abfss://sales-lakehouse@onelake.dfs.fabric.microsoft.com/Gold/sales_aggregations"
)
except Exception as e:
mssparkutils.notebook.exit(f"TECHNICAL_ERROR: Gold write failed. {str(e)[:500]}")
# Success
mssparkutils.notebook.exit("SUCCESS")
In the pipeline, after the Notebook activity, you add a Set Variable activity in the failure path that captures:
ErrorMessage:
@concat(
'Notebook [Transform_Gold] failed. ',
'Exit value: ', activity('Notebook_Gold').output.result.exitValue,
' | Spark error: ', activity('Notebook_Gold').error.message
)
FailureType:
@if(
startsWith(activity('Notebook_Gold').output.result.exitValue, 'BUSINESS_ERROR'),
'UserError',
'SystemError'
)
This is powerful: you're using the notebook's own exit value to classify the error type at the pipeline level, enabling genuinely intelligent routing. A BUSINESS_ERROR (empty table) should immediately escalate to the data team. A TECHNICAL_ERROR from a Spark executor crash might be worth an automatic retry.
Tip
Keep notebook exit values short and structured. The @activity().output.result.exitValue field has a size limit, and if your error message is a full Python stack trace, it will be truncated. Prefix exit values with a short category token (SUCCESS, BUSINESS_ERROR, TECHNICAL_ERROR) that the pipeline can parse with startsWith() expressions, and log full details to a Delta table or lakehouse file separately.
Now we converge all three failure paths — each having set our pipeline variables — onto the If_IsRetriable If Condition activity.
The If Condition activity evaluates a boolean expression and routes to either a True activities block or a False activities block. Each block is essentially a mini-pipeline canvas where you can add any activities.
The routing expression:
@equals(variables('FailureType'), 'SystemError')
This evaluates to true for infrastructure failures (potentially retriable) and false for everything else.
True branch (SystemError — potentially retriable):
Add a single Office 365 Outlook activity named Email_RetryAlert with subject:
@concat('[AUTO-RETRY CANDIDATE] Fabric Pipeline Failure — ', pipeline().Pipeline, ' at ', formatDateTime(utcNow(), 'yyyy-MM-dd HH:mm'))
And body (HTML):
@concat(
'<h2>Pipeline Failure — Potentially Retriable</h2>',
'<table>',
'<tr><td><b>Pipeline:</b></td><td>', pipeline().Pipeline, '</td></tr>',
'<tr><td><b>Run ID:</b></td><td>', pipeline().RunId, '</td></tr>',
'<tr><td><b>Failed Activity:</b></td><td>', variables('ErrorSource'), '</td></tr>',
'<tr><td><b>Error Code:</b></td><td>', variables('ErrorCode'), '</td></tr>',
'<tr><td><b>Failure Type:</b></td><td>', variables('FailureType'), '</td></tr>',
'<tr><td><b>Message:</b></td><td>', variables('ErrorMessage'), '</td></tr>',
'</table>',
'<p>This failure type is classified as a system/infrastructure error and may resolve on retry. ',
'Review the <a href="https://app.fabric.microsoft.com">Monitoring Hub</a> before manually re-triggering.</p>'
)
False branch (UserError / RequiresInvestigation — escalate immediately):
Add a single Office 365 Outlook activity named Email_EscalationAlert with subject:
@concat('[ACTION REQUIRED] Fabric Pipeline Failure — ', pipeline().Pipeline, ' at ', formatDateTime(utcNow(), 'yyyy-MM-dd HH:mm'))
And body with higher urgency language that includes the same error details plus an explicit call to action. Set the importance to High.
Note
The Office 365 Outlook activity requires a connection configured using an organizational account. That account must have an active Exchange Online mailbox. If you're using a service principal or a shared mailbox, test this connection explicitly before deploying to production — authentication silently fails in some tenant configurations and the activity will show as succeeded while no email was sent.
Let me walk you through the exact wiring process in the Fabric pipeline canvas. The goal is to implement the topology we described earlier without creating ambiguous or circular dependencies.
Step 1: Create the main success path
Add your three main activities in sequence with Success dependencies:
Copy_Bronze → Dataflow_Silver (Success)Dataflow_Silver → Notebook_Gold (Success)Notebook_Gold → Email_Success (Success)Step 2: Add Set Variable activities for each failure path
Add three Set Variable activities (one per stage) to the canvas outside the main flow:
SetError_CopySetError_DataflowSetError_NotebookConnect each one with a Failure dependency from its corresponding activity:
Copy_Bronze → SetError_Copy (Failure)Dataflow_Silver → SetError_Dataflow (Failure)Notebook_Gold → SetError_Notebook (Failure)Step 3: Add the If Condition activity
Add If_IsRetriable to the canvas. Connect all three Set Variable activities to it with Success dependencies:
SetError_Copy → If_IsRetriable (Success)SetError_Dataflow → If_IsRetriable (Success)SetError_Notebook → If_IsRetriable (Success)This means If_IsRetriable will only run after one of the Set Variable activities has successfully captured the error context.
Step 4: Wire the If Condition branches
Open the If Condition activity. In the True Activities canvas, add Email_RetryAlert. In the False Activities canvas, add Email_EscalationAlert.
Step 5: Verify the graph
At this point, your complete pipeline has two distinct flows:
The Email_Success activity has no failure path — if the success notification itself fails, you probably don't want that to trigger another alert spiral. Accept that as a tolerable gap.
Warning
A common mistake is connecting the Set Variable activities with Completion (rather than Failure) dependencies from the main activities. This would cause the Set Variable activities to run on both success and failure, meaning the If Condition would always fire — and you'd always get a failure alert even on successful runs. Use Failure-only dependencies for the error capture Set Variable activities.
The flat topology above works well for sequential pipelines with three to five stages. But what if you're implementing a medallion architecture with twelve source tables being copied in parallel, each potentially failing independently?
In this scenario, the single-activity error handler pattern produces an unreadable tangle of dependency lines. The better approach is a ForEach-based parallel ingestion pattern with a centralized post-run assessment.
Here's the architecture:
[Lookup_SourceTableList]
│ (Success)
[ForEach_Tables]
├── [Copy_Table] ──(Failure)──► [SetVar_IncrementFailCount]
└── (Success) ──► (nothing additional — ForEach continues)
│ (Completion)
[If_AnyFailures] ── True ──► [Notebook_ErrorReport] ──► [Email_FailureSummary]
│
└── False ──► [Email_Success]
The key technique here is using a pipeline variable of type Integer named FailureCount (default 0). Inside the ForEach, on each Copy activity failure, a Set Variable activity increments the count:
@add(variables('FailureCount'), 1)
After the ForEach completes (using a Completion dependency so it runs regardless of inner failures), the If Condition evaluates:
@greater(variables('FailureCount'), 0)
If any tables failed, a Notebook activity runs to query the pipeline's own run results using the Fabric REST API and generate a structured failure report, which is then attached to the email.
Key insight
The ForEach activity's isSequential setting interacts importantly with error handling. When isSequential is false (parallel execution), inner activity failures do not stop the ForEach — other iterations continue. When isSequential is true, a failure in one iteration stops all subsequent iterations. For bulk ingestion where you want maximum throughput and accept partial failures, use parallel execution. For ordered dependencies where step N+1 depends on step N, use sequential.
For the multi-source pattern, you can write a notebook that queries the Monitoring Hub API to generate a structured failure report. This demonstrates how notebook activities can serve dual purposes — not just transforming data, but participating in the pipeline's operational intelligence.
import requests
import json
from pyspark.sql import SparkSession
import pandas as pd
from datetime import datetime
# Parameters passed from pipeline
pipeline_run_id = getArgument("pipeline_run_id") # @pipeline().RunId
workspace_id = getArgument("workspace_id") # @pipeline().DataFactory
failure_count = int(getArgument("failure_count")) # @string(variables('FailureCount'))
# Build error report
error_report = {
"pipeline_run_id": pipeline_run_id,
"workspace_id": workspace_id,
"failure_count": failure_count,
"report_generated_at": datetime.utcnow().isoformat(),
"status": "PARTIAL_FAILURE" if failure_count > 0 else "SUCCESS"
}
# Write report to lakehouse for audit trail
report_df = spark.createDataFrame([error_report])
report_df.write.format("delta") \
.mode("append") \
.save("abfss://ops-lakehouse@onelake.dfs.fabric.microsoft.com/pipeline_run_audit")
# Return structured summary for email
mssparkutils.notebook.exit(
json.dumps({
"status": "PARTIAL_FAILURE",
"failure_count": failure_count,
"run_id": pipeline_run_id,
"report_saved": True
})
)
This pattern turns your error handling into an audit trail. Every pipeline run — success or failure — writes a record to a Delta table in an operations lakehouse. You can query this table to track failure rates, identify chronically failing sources, and build SLA monitoring reports directly in Power BI. For teams implementing monitoring of Fabric capacity and pipeline activity, this complements the built-in Monitoring Hub with business-contextual failure data.
Before your pipeline-level error handler even fires, Fabric gives you activity-level retry configuration. This is your first line of defense and it's criminally underused.
For every Copy, Dataflow, and Notebook activity, the Settings tab exposes:
Here's how to configure retry policies by activity type:
Copy activities connecting to external APIs:
Dataflow Gen2 activities:
Notebook activities:
Warning
Activity-level retries consume capacity units for each attempt. On shared F-SKU capacities during peak hours, a Dataflow Gen2 with 3 retries at 30-second intervals during a transient failure period can throttle other workloads for 90+ seconds. Calibrate retry counts against your capacity tier and the acceptable latency budget for the pipeline's SLA.
The difference between a useful alert email and one that gets ignored after the first week is specificity. Here are the principles for designing alert content that engineers will actually respond to:
1. Lead with what failed, not that something failed
Bad subject: Pipeline Run Failed
Good subject: [ACTION REQUIRED] Copy_Bronze failed — SalesAPI returned 503 | Pipeline: Nightly_Sales_Medallion | 2024-01-15 02:17 UTC
2. Tell them where to look
Include a direct link to the Monitoring Hub run:
@concat(
'https://app.fabric.microsoft.com/groups/',
pipeline().DataFactory,
'/monitoring'
)
Note: As of current Fabric releases, deep-linking directly to a specific pipeline run ID is not yet supported in the portal URL scheme. Linking to the workspace monitoring page is the closest available option.
3. Include the RunId prominently
The RunId is the single most useful piece of information for correlating the email alert with the Monitoring Hub log. Make it copy-pasteable.
4. Distinguish severity
Use HTML formatting in the email body to visually distinguish CRITICAL (complete data loss risk) from WARNING (partial failure, downstream impact) from INFO (non-blocking anomaly). Fabric's Office 365 Outlook activity supports HTML bodies, so use color-coded headers:
<h2 style="color: #cc0000;">🔴 CRITICAL: Pipeline Failure Requires Immediate Action</h2>
5. Include the blast radius
For partial failures in a multi-source pipeline, tell the reader which downstream datasets or reports will be stale:
@concat(
variables('FailureCount'),
' of ', variables('TotalSourceCount'),
' source tables failed to load. ',
'The following Power BI datasets may show stale data: Sales_Dashboard, Regional_Performance.'
)
Build the complete error handling pipeline described in this lesson using the following scenario:
Scenario: You have a nightly pipeline that copies order data from an Azure SQL Database to a Bronze lakehouse table, runs a Dataflow Gen2 to clean and deduplicate it into Silver, and runs a notebook to compute daily KPIs into a Gold table. Implement end-to-end error handling.
Step 1: Set up pipeline variables
Create the five pipeline variables: ErrorMessage, ErrorCode, ErrorSource, FailureType, and PipelineRunId.
Step 2: Build the happy path Add Copy, Dataflow, Notebook, and Email Success activities connected with Success dependencies.
Step 3: Add failure capture for the Copy activity
ErrorSource to the string literal "Copy_Bronze"ErrorMessage to @activity('Copy_Bronze').error.messageFailureType to @activity('Copy_Bronze').error.failureTypeStep 4: Add failure capture for the Dataflow activity
ErrorSource to "Dataflow_Silver"FailureType to "RequiresInvestigation" (literal string, overriding Dataflow's unreliable classification)Step 5: Add failure capture for the Notebook activity
mssparkutils.notebook.exit() pattern that uses BUSINESS_ERROR: or TECHNICAL_ERROR: prefixesStep 6: Wire the If Condition
@equals(variables('FailureType'), 'SystemError')Step 7: Test each failure path
mssparkutils.notebook.exit("BUSINESS_ERROR: Test failure"). Verify the False branch fires.Mistake 1: Forgetting that Skipped is contagious
If your Copy activity fails and you have Copy → Dataflow (Success only), the Dataflow gets Skipped. If your error handler depends on the Dataflow with a Failure dependency, it will not fire for a Copy failure. Always design your failure paths to originate directly from the activity that can fail, not transitively through a downstream activity.
Mistake 2: Using Set Variable in parallel without understanding race conditions
Inside a ForEach activity, multiple iterations can attempt to set the same pipeline variable simultaneously. Variables are pipeline-scoped, and concurrent writes can result in non-deterministic values. Use the FailureCount increment pattern carefully — the @add(variables('FailureCount'), 1) expression is not atomic in a parallel ForEach. For parallel scenarios, append to a String variable with a delimiter instead of incrementing an Integer, and parse the string afterward.
Mistake 3: Assuming activity().error exists on success
If you reference @activity('Copy_Bronze').error.message in an activity that runs on both Success and Failure paths (using Completion dependency), the expression will fail at evaluation when the activity succeeded because the error property doesn't exist. Use @if(equals(activity('Copy_Bronze').status, 'Failed'), activity('Copy_Bronze').error.message, 'No error') to safely access error properties conditionally.
Mistake 4: Email activity succeeding but no email received
The Office 365 Outlook activity's connection uses delegated permissions — the email is sent as the connected user account. If that account's mailbox is full, disabled, or the account has been removed from Exchange Online, the activity will still show as Succeeded in the pipeline (because the API call was accepted) but the email will bounce or be silently dropped. Monitor the connected account's mailbox health separately.
Mistake 5: The If Condition's expression returning a non-boolean
The If Condition expression must evaluate to exactly true or false (boolean). If your expression returns a string "true" instead of boolean true, the If Condition will either error or always take the False branch. Always use the built-in comparison functions (equals(), greater(), startsWith(), etc.) which return proper booleans, rather than string comparisons.
Troubleshooting: Error path fires on every run
Open the pipeline's run history in the Monitoring Hub and expand the activity tree. Look for which activities have a Completion dependency where they should have Failure-only. A single misclick on the dependency condition dropdown during canvas editing is the most common root cause.
Troubleshooting: FailureType variable is empty when If Condition evaluates
This happens when the dependency graph has a gap — the Set Variable activity ran but its downstream connection to the If Condition is missing, or the wrong dependency type was used. In the Monitoring Hub, click into the specific activity instance and verify its input/output. If the Set Variable shows as Succeeded and the variable value is populated, but the If Condition shows an empty value, check whether you have multiple If Condition activities and the wrong one is connected.
You've now built a production-grade error handling system for Microsoft Fabric pipelines. The patterns covered here — dependency condition routing, centralized error capture via Set Variable, dynamic If Condition branching on FailureType, and actionable email alerts via Office 365 Outlook — form a complete defensive layer around your data pipelines.
The key conceptual takeaways:
Where to go next:
For teams running incremental loads with watermark patterns, error handling takes on additional complexity: when a watermark-based pipeline fails mid-run, you need to decide whether to roll back the watermark or accept partial progress. Combine the patterns in this lesson with Delta Lake time travel to build truly recoverable incremental pipelines.
If your pipelines are parameterized and dynamically generated, explore using notebook variables and parameters for dynamic lakehouse ingestion — the same parameter passing mechanism that enables dynamic ingestion also enables you to pass rich context (including failure metadata) into diagnostic notebooks.
Finally, for teams who need to promote their hardened error handling pipelines across Dev/Test/Prod environments, Fabric Git Integration and Deployment Pipelines shows how to version-control your pipeline definitions and deploy them systematically — so your error handling logic ships with the same rigor as your transformation logic.
Microsoft Fabric Fundamentals
Deduplicating and Cleansing Lakehouse Delta Tables with PySpark: Drop Duplicates, Fill Nulls, and Enforce Data Quality Rules Across Medallion Layers
Implementing Table Partitioning in a Fabric Lakehouse: Choosing Partition Keys, Writing Partitioned Delta Tables with PySpark, and Pruning Partitions for Faster SQL and Spark Queries