Skip to content

Commit 2adb39d

Browse files
Merge pull request #307603 from nytian/exception
Update Error Hanldings at Durable Functions
2 parents 432895e + 8c8c98e commit 2adb39d

1 file changed

Lines changed: 214 additions & 8 deletions

File tree

articles/azure-functions/durable/durable-functions-error-handling.md

Lines changed: 214 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ Durable Function orchestrations are implemented in code and can use the programm
1515

1616
[!INCLUDE [functions-nodejs-durable-model-description](../../../includes/functions-nodejs-durable-model-description.md)]
1717

18-
## Errors in activity functions
18+
## Errors in activity functions and sub-orchestrations
1919

20-
Any exception that is thrown in an activity function is marshaled back to the orchestrator function and thrown as a `FunctionFailedException`. You can write error handling and compensation code that suits your needs in the orchestrator function.
20+
In Durable Functions, unhandled exceptions thrown within activity functions or sub-orchestrations are marshaled back to the orchestrator function using standardized exception types.
2121

22-
For example, consider the following orchestrator function that transfers funds from one account to another:
22+
For example, consider the following orchestrator function that performs a fund transfer between two accounts:
2323

2424
# [C# (InProc)](#tab/csharp-inproc)
25+
In Durable Functions C# in-process, unhandled exceptions are thrown as [FunctionFailedException](/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.functionfailedexception).
26+
27+
The exception message typically identifies which activity functions or sub-orchestrations caused the failure. To access more detailed error information, inspect the `InnerException` property.
2528

2629
```csharp
2730
[FunctionName("TransferFunds")]
@@ -45,7 +48,7 @@ public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext
4548
Amount = transferDetails.Amount
4649
});
4750
}
48-
catch (Exception)
51+
catch (FunctionFailedException)
4952
{
5053
// Refund the source account.
5154
// Another try/catch could be used here based on the needs of the application.
@@ -64,6 +67,10 @@ public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext
6467
6568
# [C# (Isolated)](#tab/csharp-isolated)
6669

70+
In Durable Functions C# Isolated, unhandled exceptions are surfaced as [TaskFailedException](/dotnet/api/microsoft.durabletask.taskfailedexception).
71+
72+
The exception message typically identifies which activity functions or sub-orchestrations caused the failure. To access more detailed error information, inspect the [FailureDetails](/dotnet/api/microsoft.durabletask.taskfailuredetails) property.
73+
6774
```csharp
6875
[FunctionName("TransferFunds")]
6976
public static async Task Run(
@@ -85,7 +92,7 @@ public static async Task Run(
8592
Amount = transferDetails.Amount
8693
});
8794
}
88-
catch (Exception)
95+
catch (TaskFailedException)
8996
{
9097
// Refund the source account.
9198
// Another try/catch could be used here based on the needs of the application.
@@ -99,6 +106,10 @@ public static async Task Run(
99106
}
100107
```
101108

109+
> [!NOTE]
110+
> - The exception message typically identifies which activity functions or sub-orchestrations caused the failure. To access more detailed error information, inspect the [`FailureDetails`](/dotnet/api/microsoft.durabletask.taskfailuredetails) property.
111+
> - By default, `FailureDetails` includes the **error type**, **error message**, **stack trace**, and any **nested inner exceptions** (each represented as a recursive `FailureDetails` object). If you want to include additional exception properties in the failure output, see [Include Custom Exception Properties for FailureDetails (.NET Isolated)](#include-custom-exception-properties-for-failuredetails-net-isolated).
112+
102113
# [JavaScript (PM3)](#tab/javascript-v3)
103114

104115
```javascript
@@ -185,7 +196,7 @@ main = df.Orchestrator.create(orchestrator_function)
185196
```
186197
# [PowerShell](#tab/powershell)
187198

188-
By default, cmdlets in PowerShell do not raise exceptions that can be caught using try/catch blocks. You have two options for changing this behavior:
199+
By default, cmdlets in PowerShell don't raise exceptions that can be caught using try/catch blocks. You have two options for changing this behavior:
189200

190201
1. Use the `-ErrorAction Stop` flag when invoking cmdlets, such as `Invoke-DurableActivity`.
191202
2. Set the [`$ErrorActionPreference`](/powershell/module/microsoft.powershell.core/about/about_preference_variables#erroractionpreference) preference variable to `"Stop"` in the orchestrator function before invoking cmdlets.
@@ -235,6 +246,122 @@ public void transferFunds(
235246

236247
If the first **CreditAccount** function call fails, the orchestrator function compensates by crediting the funds back to the source account.
237248

249+
## Errors in entity functions
250+
Exception handling behavior for entity functions differs based on the Durable Functions hosting model:
251+
252+
# [C# (InProc)](#tab/csharp-inproc)
253+
254+
In Durable Functions using C# in-process, original exception types thrown by entity functions are directly returned to the orchestrator.
255+
256+
```csharp
257+
[FunctionName("Function1")]
258+
public static async Task<string> RunOrchestrator(
259+
[OrchestrationTrigger] IDurableOrchestrationContext context)
260+
{
261+
try
262+
{
263+
var entityId = new EntityId(nameof(Counter), "myCounter");
264+
await context.CallEntityAsync(entityId, "Add", 1);
265+
}
266+
catch (Exception ex)
267+
{
268+
// The exception type will be InvalidOperationException with the message "this is an entity exception".
269+
}
270+
return string.Empty;
271+
}
272+
273+
[FunctionName("Counter")]
274+
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
275+
{
276+
switch (ctx.OperationName.ToLowerInvariant())
277+
{
278+
case "add":
279+
throw new InvalidOperationException("this is an entity exception");
280+
case "get":
281+
ctx.Return(ctx.GetState<int>());
282+
break;
283+
}
284+
}
285+
```
286+
287+
# [C# (Isolated)](#tab/csharp-isolated)
288+
289+
In Durable Functions C# Isolated, exceptions are surfaced to the orchestrator as an `EntityOperationFailedException`. To access the original exception details, inspect its `FailureDetails` property.
290+
291+
```csharp
292+
[Function(nameof(MyOrchestrator))]
293+
public static async Task<List<string>> MyOrchestrator(
294+
[Microsoft.Azure.Functions.Worker.OrchestrationTrigger] TaskOrchestrationContext context)
295+
{
296+
var entityId = new Microsoft.DurableTask.Entities.EntityInstanceId(nameof(Counter), "myCounter");
297+
try
298+
{
299+
await context.Entities.CallEntityAsync(entityId, "Add", 1);
300+
}
301+
catch (EntityOperationFailedException ex)
302+
{
303+
// Add your error handling
304+
}
305+
306+
return new List<string>();
307+
}
308+
```
309+
# [JavaScript (PM3)](#tab/javascript-v3)
310+
311+
```javascript
312+
df.app.orchestration("counterOrchestration", function* (context) {
313+
const entityId = new df.EntityId(counterEntityName, "myCounter");
314+
315+
try {
316+
const currentValue = yield context.df.callEntity(entityId, "get");
317+
if (currentValue < 10) {
318+
yield context.df.callEntity(entityId, "add", 1);
319+
}
320+
} catch (err) {
321+
context.log(`Entity call failed: ${err.message ?? err}`);
322+
}
323+
});
324+
```
325+
326+
# [JavaScript (PM4)](#tab/javascript-v4)
327+
328+
```javascript
329+
df.app.orchestration("counterOrchestration", function* (context) {
330+
const entityId = new df.EntityId(counterEntityName, "myCounter");
331+
332+
try {
333+
const currentValue = yield context.df.callEntity(entityId, "get");
334+
if (currentValue < 10) {
335+
yield context.df.callEntity(entityId, "add", 1);
336+
}
337+
} catch (err) {
338+
context.log(`Entity call failed: ${err.message ?? err}`);
339+
}
340+
});
341+
```
342+
343+
# [Python](#tab/python)
344+
345+
```python
346+
@myApp.orchestration_trigger(context_name="context")
347+
def run_orchestrator(context):
348+
try:
349+
entityId = df.EntityId("Counter", "myCounter")
350+
yield context.call_entity(entityId, "get")
351+
return "finished"
352+
except Exception as e:
353+
# Add your error handling
354+
```
355+
# [PowerShell](#tab/powershell)
356+
357+
Entity functions aren't currently not supported in PowerShell.
358+
359+
# [Java](#tab/java)
360+
361+
Entity functions aren't currently not supported in Java.
362+
363+
---
364+
238365
## Automatic retry on failure
239366
240367
When you call activity functions or sub-orchestration functions, you can specify an automatic retry policy. The following example attempts to call a function up to three times and waits 5 seconds between each retry:
@@ -365,7 +492,7 @@ The activity function call in the previous example takes a parameter for configu
365492
366493
## Custom retry handlers
367494
368-
When using the .NET or Java, you also have the option to implement retry handlers in code. This is useful when declarative retry policies are not expressive enough. For languages that don't support custom retry handlers, you still have the option of implementing retry policies using loops, exception handling, and timers for injecting delays between retries.
495+
When using the .NET or Java, you also have the option to implement retry handlers in code. This is useful when declarative retry policies aren't expressive enough. For languages that don't support custom retry handlers, you still have the option of implementing retry policies using loops, exception handling, and timers for injecting delays between retries.
369496
370497
# [C# (InProc)](#tab/csharp-inproc)
371498
@@ -636,12 +763,91 @@ public boolean timerOrchestrator(
636763
---
637764
638765
> [!NOTE]
639-
> This mechanism does not actually terminate in-progress activity function execution. Rather, it simply allows the orchestrator function to ignore the result and move on. For more information, see the [Timers](durable-functions-timers.md#usage-for-timeout) documentation.
766+
> This mechanism doesn't actually terminate in-progress activity function execution. Rather, it simply allows the orchestrator function to ignore the result and move on. For more information, see the [Timers](durable-functions-timers.md#usage-for-timeout) documentation.
640767
641768
## Unhandled exceptions
642769
643770
If an orchestrator function fails with an unhandled exception, the details of the exception are logged and the instance completes with a `Failed` status.
644771
772+
## Include Custom Exception Properties for FailureDetails (.NET Isolated)
773+
774+
When running Durable Task workflows in the .NET Isolated model, task failures are automatically serialized into a FailureDetails object. By default, this object includes standard fields such as:
775+
- ErrorType — the exception type name
776+
- Message — the exception message
777+
- StackTrace — the serialized stack trace
778+
- InnerFailure – a nested FailureDetails object for recursive inner exceptions
779+
780+
Starting with Microsoft.Azure.Functions.Worker.Extensions.DurableTask [v1.9.0](https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.DurableTask/1.9.0), You can extend this behavior by implementing an IExceptionPropertiesProvider (defined in the Microsoft.DurableTask.Worker starting from [v1.16.1](https://www.nuget.org/packages/Microsoft.DurableTask.Worker/1.16.1)package). This provider defines which exception types and which of their properties should be included in the FailureDetails.Properties dictionary.
781+
782+
> [!NOTE]
783+
> - This feature is available in **.NET Isolated** only. Support for Java will be added in a future release.
784+
> - Make sure you're using **Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.9.0** or later.
785+
> - Make sure you're using **Microsoft.DurableTask.Worker v1.16.1** or later.
786+
787+
### Implement an Exception Properties Provider
788+
Implement a custom IExceptionPropertiesProvider to extract and return selected properties for the exceptions you care about. The returned dictionary will be serialized into the Properties field of FailureDetails when a matching exception type is thrown.
789+
790+
```csharp
791+
using Microsoft.DurableTask.Worker;
792+
793+
public class CustomExceptionPropertiesProvider : IExceptionPropertiesProvider
794+
{
795+
public IDictionary<string, object?>? GetExceptionProperties(Exception exception)
796+
{
797+
return exception switch
798+
{
799+
ArgumentOutOfRangeException e => new Dictionary<string, object?>
800+
{
801+
["ParamName"] = e.ParamName,
802+
["ActualValue"] = e.ActualValue
803+
},
804+
InvalidOperationException e => new Dictionary<string, object?>
805+
{
806+
["CustomHint"] = "Invalid operation occurred",
807+
["TimestampUtc"] = DateTime.UtcNow
808+
},
809+
_ => null // Other exception types not handled
810+
};
811+
}
812+
}
813+
```
814+
815+
### Register the Provider
816+
Register your custom IExceptionPropertiesProvider in your .NET Isolated worker host, typically in Program.cs:
817+
```csharp
818+
using Microsoft.DurableTask.Worker;
819+
using Microsoft.Extensions.DependencyInjection;
820+
821+
var host = new HostBuilder()
822+
.ConfigureFunctionsWorkerDefaults(builder =>
823+
{
824+
// Register custom exception properties provider
825+
builder.Services.AddSingleton<IExceptionPropertiesProvider, CustomExceptionPropertiesProvider>();
826+
})
827+
.Build();
828+
829+
host.Run();
830+
```
831+
Once registered, any exception that matches one of the handled types will automatically include the configured properties in its FailureDetails.
832+
833+
### Sample FailureDetails Output
834+
When an exception occurs that matches your provider’s configuration, the orchestration receives a serialized FailureDetails structure like this:
835+
```json
836+
{
837+
"errorType": "TaskFailedException",
838+
"message": "Activity failed with an exception.",
839+
"stackTrace": "...",
840+
"innerFailure": {
841+
"errorType": "ArgumentOutOfRangeException",
842+
"message": "Specified argument was out of range.",
843+
"properties": {
844+
"ParamName": "count",
845+
"ActualValue": 42
846+
}
847+
}
848+
}
849+
```
850+
645851
## Next steps
646852
647853
> [!div class="nextstepaction"]

0 commit comments

Comments
 (0)