| title | Handling errors and Retries |
|---|---|
| description | Learn how to handle errors in the Durable Functions extension for Azure Functions and Durable Task SDKs. |
| ms.topic | how-to |
| ms.service | durable-task |
| ms.date | 02/04/2026 |
| ms.author | hannahhunter |
| author | hhunter-ms |
| ms.devlang | csharp |
| ms.custom | devx-track-js |
| zone_pivot_groups | azure-durable-approach |
::: zone pivot="durable-functions"
You implement Durable Functions orchestrations in code, so you use your language's built-in error handling features. Error handling and compensation don't require new concepts, but a few orchestration behaviors are worth knowing about.
[!INCLUDE functions-nodejs-durable-model-description]
::: zone-end
::: zone pivot="durable-task-sdks"
Apps that use cloud services need to handle failures, and client side retries are an important part of the design. The Durable Task SDKs include support for error handling, retries, and timeouts to help you build robust workflows.
::: zone-end
::: zone pivot="durable-functions"
In Durable Functions, unhandled exceptions thrown within activity functions or sub-orchestrations are marshaled back to the orchestrator function using standardized exception types.
The following orchestrator function transfers funds between two accounts:
Isolated worker model
In Durable Functions C# Isolated, unhandled exceptions are surfaced as TaskFailedException.
The exception message typically identifies which activity functions or sub-orchestrations caused the failure. To access more detailed error information, inspect the FailureDetails property.
[FunctionName("TransferFunds")]
public static async Task Run(
[OrchestrationTrigger] TaskOrchestrationContext context, TransferOperation transferDetails)
{
await context.CallActivityAsync("DebitAccount",
new
{
Account = transferDetails.SourceAccount,
Amount = transferDetails.Amount
});
try
{
await context.CallActivityAsync("CreditAccount",
new
{
Account = transferDetails.DestinationAccount,
Amount = transferDetails.Amount
});
}
catch (TaskFailedException)
{
// Refund the source account.
// Another try/catch could be used here based on the needs of the application.
await context.CallActivityAsync("CreditAccount",
new
{
Account = transferDetails.SourceAccount,
Amount = transferDetails.Amount
});
}
}[!NOTE]
- The exception message typically identifies which activity functions or sub-orchestrations caused the failure. To access more detailed error information, inspect the
FailureDetailsproperty.- By default,
FailureDetailsincludes the error type, error message, stack trace, and any nested inner exceptions (each represented as a recursiveFailureDetailsobject). To include additional exception properties in the failure output, see Include Custom Exception Properties for FailureDetails (.NET Isolated).
[!IMPORTANT] Migration note (in-process to isolated): In the in-process model,
FunctionFailedException.InnerExceptioncontains the original exception object thrown by the activity, which you can cast and inspect directly. In the isolated worker model,TaskFailedExceptiondoes not contain the original exception as anInnerException. Instead, error details are available only through theFailureDetailsproperty, which provides string-based properties (ErrorType,ErrorMessage,StackTrace). You can't cast or access the original exception object directly. UseFailureDetails.IsCausedBy<T>()to check the original exception type.
In-process model
In Durable Functions C# in-process, unhandled exceptions are thrown as FunctionFailedException.
The exception message usually includes the activity function or sub-orchestration that failed. For details, inspect InnerException.
[FunctionName("TransferFunds")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var transferDetails = context.GetInput<TransferOperation>();
await context.CallActivityAsync("DebitAccount",
new
{
Account = transferDetails.SourceAccount,
Amount = transferDetails.Amount
});
try
{
await context.CallActivityAsync("CreditAccount",
new
{
Account = transferDetails.DestinationAccount,
Amount = transferDetails.Amount
});
}
catch (FunctionFailedException)
{
// Refund the source account.
// Another try/catch could be used here based on the needs of the application.
await context.CallActivityAsync("CreditAccount",
new
{
Account = transferDetails.SourceAccount,
Amount = transferDetails.Amount
});
}
}[!NOTE] The previous C# examples use Durable Functions 2.x. For Durable Functions 1.x, use
DurableOrchestrationContextinstead ofIDurableOrchestrationContext. For version differences, see the Durable Functions versions article.
V3 programming model
const df = require("durable-functions");
module.exports = df.orchestrator(function* (context) {
const transferDetails = context.df.getInput();
yield context.df.callActivity("DebitAccount", {
account: transferDetails.sourceAccount,
amount: transferDetails.amount,
});
try {
yield context.df.callActivity("CreditAccount", {
account: transferDetails.destinationAccount,
amount: transferDetails.amount,
});
} catch (error) {
// Refund the source account.
// Another try/catch could be used here based on the needs of the application.
yield context.df.callActivity("CreditAccount", {
account: transferDetails.sourceAccount,
amount: transferDetails.amount,
});
}
})V4 programming model
const df = require("durable-functions");
df.app.orchestration("transferFunds", function* (context) {
const transferDetails = context.df.getInput();
yield context.df.callActivity("debitAccount", {
account: transferDetails.sourceAccount,
amount: transferDetails.amount,
});
try {
yield context.df.callActivity("creditAccount", {
account: transferDetails.destinationAccount,
amount: transferDetails.amount,
});
} catch (error) {
// Refund the source account.
// Another try/catch could be used here based on the needs of the application.
yield context.df.callActivity("creditAccount", {
account: transferDetails.sourceAccount,
amount: transferDetails.amount,
});
}
});import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
transfer_details = context.get_input()
yield context.call_activity('DebitAccount', {
'account': transfer_details['sourceAccount'],
'amount' : transfer_details['amount']
})
try:
yield context.call_activity('CreditAccount', {
'account': transfer_details['destinationAccount'],
'amount': transfer_details['amount'],
})
except Exception:
yield context.call_activity('CreditAccount', {
'account': transfer_details['sourceAccount'],
'amount': transfer_details['amount']
})
main = df.Orchestrator.create(orchestrator_function)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:
- Use the
-ErrorAction Stopflag when invoking cmdlets, such asInvoke-DurableActivity. - Set the
$ErrorActionPreferencepreference variable to"Stop"in the orchestrator function before invoking cmdlets.
param($Context)
$ErrorActionPreference = "Stop"
$transferDetails = $Context.Input
Invoke-DurableActivity -FunctionName 'DebitAccount' -Input @{ account = $transferDetails.sourceAccount; amount = $transferDetails.amount }
try {
Invoke-DurableActivity -FunctionName 'CreditAccount' -Input @{ account = $transferDetails.destinationAccount; amount = $transferDetails.amount }
} catch {
Invoke-DurableActivity -FunctionName 'CreditAccount' -Input @{ account = $transferDetails.sourceAccount; amount = $transferDetails.amount }
}For more information on error handling in PowerShell, see the Try-Catch-Finally PowerShell documentation.
@FunctionName("TransferFunds")
public void transferFunds(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
TransferOperation transfer = ctx.getInput(TransferOperation.class);
ctx.callActivity(
"DebitAccount",
new OperationArgs(transfer.sourceAccount, transfer.amount)).await();
try {
ctx.callActivity(
"CreditAccount",
new OperationArgs(transfer.destinationAccount, transfer.amount)).await();
} catch (TaskFailedException ex) {
// Refund the source account on failure
ctx.callActivity(
"CreditAccount",
new OperationArgs(transfer.sourceAccount, transfer.amount)).await();
}
}If the first CreditAccount function call fails, the orchestrator function compensates by crediting the funds back to the source account.
::: zone-end
::: zone pivot="durable-task-sdks"
In the Durable Task SDKs, unhandled exceptions thrown within activities or sub-orchestrations are marshaled back to the orchestrator using the TaskFailedException type. The exception's FailureDetails property provides detailed information about the failure.
using Microsoft.DurableTask;
[DurableTask(nameof(TransferFundsOrchestration))]
public class TransferFundsOrchestration : TaskOrchestrator<TransferOperation, string>
{
public override async Task<string> RunAsync(
TaskOrchestrationContext context, TransferOperation transfer)
{
await context.CallActivityAsync(
nameof(DebitAccountActivity),
new AccountOperation { Account = transfer.SourceAccount, Amount = transfer.Amount });
try
{
await context.CallActivityAsync(
nameof(CreditAccountActivity),
new AccountOperation { Account = transfer.DestinationAccount, Amount = transfer.Amount });
}
catch (TaskFailedException ex)
{
// Log the failure details
var details = ex.FailureDetails;
// Compensate by refunding the source account
await context.CallActivityAsync(
nameof(CreditAccountActivity),
new AccountOperation { Account = transfer.SourceAccount, Amount = transfer.Amount });
return $"Transfer failed: {details.ErrorMessage}. Compensation completed.";
}
return "Transfer completed successfully";
}
}import { OrchestrationContext, TOrchestrator } from "@microsoft/durabletask-js";
const transferFundsOrchestrator: TOrchestrator = async function* (
ctx: OrchestrationContext
): any {
const transfer = ctx.getInput() as {
sourceAccount: string;
destinationAccount: string;
amount: number;
};
yield ctx.callActivity(debitAccount, {
account: transfer.sourceAccount,
amount: transfer.amount,
});
try {
yield ctx.callActivity(creditAccount, {
account: transfer.destinationAccount,
amount: transfer.amount,
});
} catch (error) {
// Compensate by refunding the source account
yield ctx.callActivity(creditAccount, {
account: transfer.sourceAccount,
amount: transfer.amount,
});
return `Transfer failed. Compensation completed.`;
}
return "Transfer completed successfully";
};from durabletask import task
def transfer_funds_orchestrator(ctx: task.OrchestrationContext, transfer: dict) -> str:
"""
Orchestrator that demonstrates error handling with compensation.
"""
source_account = transfer.get("source_account")
destination_account = transfer.get("destination_account")
amount = transfer.get("amount")
# Debit the source account
yield ctx.call_activity("debit_account", input={
"account": source_account,
"amount": amount
})
try:
# Credit the destination account
yield ctx.call_activity("credit_account", input={
"account": destination_account,
"amount": amount
})
except task.TaskFailedError as ex:
# Compensate by refunding the source account
yield ctx.call_activity("credit_account", input={
"account": source_account,
"amount": amount
})
return f"Transfer failed: {ex}. Compensation completed."
return "Transfer completed successfully"No PowerShell sample is available. Use the .NET, JavaScript, Java, or Python sample.
import com.microsoft.durabletask.*;
public TaskOrchestration createTransferOrchestration() {
return ctx -> {
TransferOperation transfer = ctx.getInput(TransferOperation.class);
ctx.callActivity("DebitAccount",
new AccountOperation(transfer.sourceAccount, transfer.amount)).await();
try {
ctx.callActivity("CreditAccount",
new AccountOperation(transfer.destinationAccount, transfer.amount)).await();
} catch (TaskFailedException ex) {
// Compensate by refunding the source account
ctx.callActivity("CreditAccount",
new AccountOperation(transfer.sourceAccount, transfer.amount)).await();
ctx.complete("Transfer failed: " + ex.getMessage() + ". Compensation completed.");
return;
}
ctx.complete("Transfer completed successfully");
};
}If the CreditAccount activity fails, the orchestrator catches the exception and compensates by crediting the funds back to the source account.
::: zone-end
::: zone pivot="durable-functions"
When you use Task.WhenAll to run multiple activity calls in parallel (fan-out/fan-in pattern) and one or more activities fail, await throws only the first exception. To access all failures, inspect the Exception property on the Task returned by Task.WhenAll.
Isolated worker model
var tasks = new[]
{
context.CallActivityAsync("Activity1", input1),
context.CallActivityAsync("Activity2", input2),
context.CallActivityAsync("Activity3", input3),
};
var allTask = Task.WhenAll(tasks);
try
{
await allTask;
}
catch (TaskFailedException)
{
// 'await' rethrows only the first exception. To inspect all failures,
// check allTask.Exception, which is an AggregateException.
if (allTask.Exception != null)
{
foreach (var inner in allTask.Exception.InnerExceptions)
{
if (inner is TaskFailedException taskFailed)
{
// Use taskFailed.FailureDetails to inspect error details
var errorType = taskFailed.FailureDetails.ErrorType;
var errorMessage = taskFailed.FailureDetails.ErrorMessage;
}
}
}
}In-process model
var tasks = new[]
{
context.CallActivityAsync("Activity1", input1),
context.CallActivityAsync("Activity2", input2),
context.CallActivityAsync("Activity3", input3),
};
var allTask = Task.WhenAll(tasks);
try
{
await allTask;
}
catch (FunctionFailedException)
{
// 'await' rethrows only the first exception. To inspect all failures,
// check allTask.Exception, which is an AggregateException.
if (allTask.Exception != null)
{
foreach (var inner in allTask.Exception.InnerExceptions)
{
if (inner is FunctionFailedException funcFailed)
{
// Use funcFailed.InnerException to access the original exception
}
}
}
}In JavaScript, when you use context.df.Task.all to run multiple activity calls in parallel, the first failure causes the task to complete with an error. Wrap the call in a try/catch block to handle the error.
In Python, when you use context.task_all to run multiple activity calls in parallel, the first failure causes the task to complete with an error. Wrap the call in a try/except block to handle the error.
In PowerShell, use Wait-DurableTask with multiple tasks. If any task fails, the error is raised.
In Java, when you use ctx.allOf to run multiple activity calls in parallel, the first failure causes the task to complete with an error. Use a try/catch block to handle the error.
Exception handling in entity functions depends on the Durable Functions hosting model:
Isolated worker model
In Durable Functions C# isolated, the runtime wraps entity function exceptions in an EntityOperationFailedException. To get the original exception details, inspect the FailureDetails property.
[Function(nameof(MyOrchestrator))]
public static async Task<List<string>> MyOrchestrator(
[Microsoft.Azure.Functions.Worker.OrchestrationTrigger] TaskOrchestrationContext context)
{
var entityId = new Microsoft.DurableTask.Entities.EntityInstanceId(nameof(Counter), "myCounter");
try
{
await context.Entities.CallEntityAsync(entityId, "Add", 1);
}
catch (EntityOperationFailedException ex)
{
// Add your error handling
}
return new List<string>();
}In-process model
In Durable Functions with C# in-process, entity functions return their original exception types to the orchestrator.
[FunctionName("Function1")]
public static async Task<string> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
try
{
var entityId = new EntityId(nameof(Counter), "myCounter");
await context.CallEntityAsync(entityId, "Add", 1);
}
catch (Exception ex)
{
// The exception type is InvalidOperationException with the message "this is an entity exception".
}
return string.Empty;
}
[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
switch (ctx.OperationName.ToLowerInvariant())
{
case "add":
throw new InvalidOperationException("this is an entity exception");
case "get":
ctx.Return(ctx.GetState<int>());
break;
}
}df.app.orchestration("counterOrchestration", function* (context) {
const entityId = new df.EntityId(counterEntityName, "myCounter");
try {
const currentValue = yield context.df.callEntity(entityId, "get");
if (currentValue < 10) {
yield context.df.callEntity(entityId, "add", 1);
}
} catch (err) {
context.log(`Entity call failed: ${err.message ?? err}`);
}
});@myApp.orchestration_trigger(context_name="context")
def run_orchestrator(context):
try:
entityId = df.EntityId("Counter", "myCounter")
yield context.call_entity(entityId, "get")
return "finished"
except Exception as e:
# Add your error handlingPowerShell doesn't support entity functions.
Java doesn't support entity functions.
::: zone-end
::: zone pivot="durable-functions"
When you call activity functions or sub-orchestration functions, specify an automatic retry policy. The following example calls a function up to three times and waits five seconds between retries:
Isolated worker model
[FunctionName("TimerOrchestratorWithRetry")]
public static async Task Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
var options = TaskOptions.FromRetryPolicy(new RetryPolicy(
maxNumberOfAttempts: 3,
firstRetryInterval: TimeSpan.FromSeconds(5)));
await context.CallActivityAsync("FlakyFunction", options: options);
// ...
}In-process model
[FunctionName("TimerOrchestratorWithRetry")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var retryOptions = new RetryOptions(
firstRetryInterval: TimeSpan.FromSeconds(5),
maxNumberOfAttempts: 3);
await context.CallActivityWithRetryAsync("FlakyFunction", retryOptions, null);
// ...
}[!NOTE] The previous C# examples are for Durable Functions 2.x. For Durable Functions 1.x, you must use
DurableOrchestrationContextinstead ofIDurableOrchestrationContext. For more information about the differences between versions, see the Durable Functions versions article.
V3 programming model
const df = require("durable-functions");
module.exports = df.orchestrator(function*(context) {
const firstRetryIntervalInMilliseconds = 5000;
const maxNumberOfAttempts = 3;
const retryOptions =
new df.RetryOptions(firstRetryIntervalInMilliseconds, maxNumberOfAttempts);
yield context.df.callActivityWithRetry("FlakyFunction", retryOptions);
// ...
});V4 programming model
const df = require("durable-functions");
df.app.orchestration("callActivityWithRetry", function* (context) {
const firstRetryIntervalInMilliseconds = 5000;
const maxNumberOfAttempts = 3;
const retryOptions = new df.RetryOptions(firstRetryIntervalInMilliseconds, maxNumberOfAttempts);
yield context.df.callActivityWithRetry("FlakyFunction", retryOptions);
// ...
});import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
first_retry_interval_in_milliseconds = 5000
max_number_of_attempts = 3
retry_options = df.RetryOptions(first_retry_interval_in_milliseconds, max_number_of_attempts)
yield context.call_activity_with_retry('FlakyFunction', retry_options)
main = df.Orchestrator.create(orchestrator_function)param($Context)
$retryOptions = New-DurableRetryOptions `
-FirstRetryInterval (New-TimeSpan -Seconds 5) `
-MaxNumberOfAttempts 3
Invoke-DurableActivity -FunctionName 'FlakyFunction' -RetryOptions $retryOptions@FunctionName("TimerOrchestratorWithRetry")
public void timerOrchestratorWithRetry(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
final int maxAttempts = 3;
final Duration firstRetryInterval = Duration.ofSeconds(5);
RetryPolicy policy = new RetryPolicy(maxAttempts, firstRetryInterval);
TaskOptions options = new TaskOptions(policy);
ctx.callActivity("FlakyFunction", options).await();
// ...
}The activity function call in the previous example uses a parameter to configure an automatic retry policy. Customize the policy with these options:
- Max number of attempts: The maximum number of attempts. If set to 1, no retries occur.
- First retry interval: The amount of time to wait before the first retry attempt.
- Backoff coefficient: The coefficient used to determine rate of increase of backoff. Defaults to 1.
- Max retry interval: The maximum amount of time to wait between retry attempts.
- Retry timeout: The maximum amount of time to spend retrying. By default, retries continue indefinitely.
::: zone-end
::: zone pivot="durable-task-sdks"
The Durable Task SDKs include alternative scheduling methods that retry failed activities based on a supplied policy. These methods are useful for activities that read data from web services or perform idempotent writes to a database.
using Microsoft.DurableTask;
[DurableTask(nameof(OrchestratorWithRetry))]
public class OrchestratorWithRetry : TaskOrchestrator<string, string>
{
public override async Task<string> RunAsync(
TaskOrchestrationContext context, string input)
{
// Configure retry policy
var retryPolicy = new RetryPolicy(
maxNumberOfAttempts: 3,
firstRetryInterval: TimeSpan.FromSeconds(5),
backoffCoefficient: 2.0,
maxRetryInterval: TimeSpan.FromMinutes(1),
retryTimeout: TimeSpan.FromMinutes(5));
var options = TaskOptions.FromRetryPolicy(retryPolicy);
// Call activity with automatic retry
string result = await context.CallActivityAsync<string>(
nameof(UnreliableActivity), input, options);
return result;
}
}import {
OrchestrationContext,
TOrchestrator,
RetryPolicy,
} from "@microsoft/durabletask-js";
const retryPolicyOrchestrator: TOrchestrator = async function* (
ctx: OrchestrationContext
): any {
const retryPolicy = new RetryPolicy({
maxNumberOfAttempts: 3,
firstRetryIntervalInMilliseconds: 5000,
backoffCoefficient: 2.0,
maxRetryIntervalInMilliseconds: 60000,
});
const result: string = yield ctx.callActivity(
unreliableActivity,
ctx.getInput(),
{ retry: retryPolicy }
);
return result;
};from durabletask import task
from durabletask.task import RetryPolicy
def orchestrator_with_retry(ctx: task.OrchestrationContext, input_data: str) -> str:
"""
Orchestrator that demonstrates automatic retry on activity failure.
"""
# Configure retry policy
retry_policy = RetryPolicy(
first_retry_interval=5, # seconds
max_number_of_attempts=3,
backoff_coefficient=2.0,
max_retry_interval=60, # seconds
retry_timeout=300 # seconds
)
# Call activity with automatic retry
result = yield ctx.call_activity(
"unreliable_activity",
input=input_data,
retry_policy=retry_policy
)
return resultThis sample is shown for .NET, JavaScript, Java, and Python.
import com.microsoft.durabletask.*;
import java.time.Duration;
public TaskOrchestration createOrchestratorWithRetry() {
return ctx -> {
String input = ctx.getInput(String.class);
// Configure retry policy
RetryPolicy retryPolicy = new RetryPolicy(
3, // maxNumberOfAttempts
Duration.ofSeconds(5), // firstRetryInterval
2.0, // backoffCoefficient
Duration.ofMinutes(1), // maxRetryInterval
Duration.ofMinutes(5) // retryTimeout
);
TaskOptions options = new TaskOptions(retryPolicy);
// Call activity with automatic retry
String result = ctx.callActivity(
"UnreliableActivity", input, options, String.class).await();
ctx.complete(result);
};
}The retry policy options are:
- Max number of attempts: The maximum number of retry attempts. If set to 1, no retries occur.
- First retry interval: The amount of time to wait before the first retry attempt.
- Backoff coefficient: The coefficient used to determine rate of increase of backoff. Defaults to 1.
- Max retry interval: The maximum amount of time to wait between retry attempts.
- Retry timeout: The maximum amount of time to spend doing retries.
::: zone-end
::: zone pivot="durable-functions"
In .NET and Java, implement retry handlers in code when declarative retry policies aren't expressive enough. In other languages, implement retry logic by using loops, exception handling, and timers to delay between retries.
Isolated worker model
TaskOptions retryOptions = TaskOptions.FromRetryHandler(retryContext =>
{
// Don't retry anything that derives from ApplicationException
if (retryContext.LastFailure.IsCausedBy<ApplicationException>())
{
return false;
}
// Quit after N attempts
return retryContext.LastAttemptNumber < 3;
});
try
{
await ctx.CallActivityAsync("FlakeyActivity", options: retryOptions);
}
catch (TaskFailedException)
{
// Case when the retry handler returns false...
}In-process model
RetryOptions retryOptions = new RetryOptions(
firstRetryInterval: TimeSpan.FromSeconds(5),
maxNumberOfAttempts: int.MaxValue)
{
Handle = exception =>
{
// Return true to handle and retry, or false to throw.
if (exception is TaskFailedException failure)
{
// Exceptions from task activities are always this type. Inspect the
// inner exception for more details.
}
return false;
}
};
await ctx.CallActivityWithRetryAsync("FlakeyActivity", retryOptions, null);JavaScript doesn't support custom retry handlers in Durable Functions. Implement retry logic in the orchestrator function by using loops, exception handling, and timers to delay between retries.
Python doesn't support custom retry handlers. Implement retry logic in the orchestrator function by using loops, exception handling, and timers to delay between retries.
PowerShell doesn't support custom retry handlers. Implement retry logic in the orchestrator function by using loops, exception handling, and timers to delay between retries.
RetryHandler retryHandler = retryCtx -> {
// Don't retry anything that derives from RuntimeException
if (retryCtx.getLastFailure().isCausedBy(RuntimeException.class)) {
return false;
}
// Quit after N attempts
return retryCtx.getLastAttemptNumber() < 3;
};
TaskOptions options = new TaskOptions(retryHandler);
try {
ctx.callActivity("FlakeyActivity", options).await();
} catch (TaskFailedException ex) {
// Case when the retry handler returns false...
}::: zone-end
::: zone pivot="durable-task-sdks"
In .NET and Java, implement retry handlers in code to control retry logic. This approach is useful when declarative retry policies aren't expressive enough.
using Microsoft.DurableTask;
[DurableTask(nameof(OrchestratorWithCustomRetry))]
public class OrchestratorWithCustomRetry : TaskOrchestrator<string, string>
{
public override async Task<string> RunAsync(
TaskOrchestrationContext context, string input)
{
// Custom retry handler with conditional logic
TaskOptions retryOptions = TaskOptions.FromRetryHandler(retryContext =>
{
// Don't retry if it's a validation error
if (retryContext.LastFailure.IsCausedBy<ArgumentException>())
{
return false;
}
// Retry up to 5 times for transient errors
return retryContext.LastAttemptNumber < 5;
});
try
{
return await context.CallActivityAsync<string>(
nameof(UnreliableActivity), input, retryOptions);
}
catch (TaskFailedException)
{
// All retries exhausted
return "Operation failed after all retries";
}
}
}import {
OrchestrationContext,
TOrchestrator,
} from "@microsoft/durabletask-js";
import type { RetryHandler, RetryContext } from "@microsoft/durabletask-js";
const customRetryHandlerOrchestrator: TOrchestrator = async function* (
ctx: OrchestrationContext
): any {
const maxAttempts = 4;
const customRetryHandler: RetryHandler = (retryCtx: RetryContext) => {
if (retryCtx.lastAttemptNumber >= maxAttempts) {
return false; // give up
}
// Only retry transient errors
if (!retryCtx.lastFailure.message?.includes("TransientError")) {
return false;
}
return true; // retry immediately
};
const result: string = yield ctx.callActivity(
unreliableActivity,
ctx.getInput(),
{ retry: customRetryHandler }
);
return result;
};Custom retry handlers aren't supported in Python. Implement custom retry logic by using loops, exception handling, and timers:
import datetime
from durabletask import task
def orchestrator_with_custom_retry(ctx: task.OrchestrationContext, input_data: str) -> str:
"""
Orchestrator that demonstrates custom retry logic.
"""
max_attempts = 5
retry_interval_seconds = 5
for attempt in range(1, max_attempts + 1):
try:
result = yield ctx.call_activity("unreliable_activity", input=input_data)
return result
except task.TaskFailedError as ex:
if attempt >= max_attempts:
return f"Operation failed after {max_attempts} attempts"
# Wait before retrying
next_retry = ctx.current_utc_datetime + datetime.timedelta(seconds=retry_interval_seconds)
yield ctx.create_timer(next_retry)
return "Unexpected error"Custom retry handlers aren't supported in PowerShell. Implement custom retry logic by using loops, exception handling, and timers.
import com.microsoft.durabletask.*;
public TaskOrchestration createOrchestratorWithCustomRetry() {
return ctx -> {
String input = ctx.getInput(String.class);
// Custom retry handler with conditional logic
RetryHandler retryHandler = retryContext -> {
// Don't retry validation errors
if (retryContext.getLastFailure().isCausedBy(IllegalArgumentException.class)) {
return false;
}
// Retry up to 5 times for transient errors
return retryContext.getLastAttemptNumber() < 5;
};
TaskOptions options = new TaskOptions(retryHandler);
try {
String result = ctx.callActivity("UnreliableActivity", input, options, String.class).await();
ctx.complete(result);
} catch (TaskFailedException ex) {
// All retries exhausted
ctx.complete("Operation failed after all retries");
}
};
}::: zone-end
::: zone pivot="durable-functions"
If a function call takes too long, time it out in the orchestrator function. Create a durable timer with an any task selector, as in the following example:
Isolated worker model
[Function("TimerOrchestrator")]
public static async Task<bool> Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
TimeSpan timeout = TimeSpan.FromSeconds(30);
DateTime deadline = context.CurrentUtcDateTime.Add(timeout);
using (var cts = new CancellationTokenSource())
{
Task activityTask = context.CallActivityAsync("FlakyFunction");
Task timeoutTask = context.CreateTimer(deadline, cts.Token);
Task winner = await Task.WhenAny(activityTask, timeoutTask);
if (winner == activityTask)
{
// success case
cts.Cancel();
return true;
}
else
{
// timeout case
return false;
}
}
}In-process model
[FunctionName("TimerOrchestrator")]
public static async Task<bool> Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
TimeSpan timeout = TimeSpan.FromSeconds(30);
DateTime deadline = context.CurrentUtcDateTime.Add(timeout);
using (var cts = new CancellationTokenSource())
{
Task activityTask = context.CallActivityAsync("FlakyFunction");
Task timeoutTask = context.CreateTimer(deadline, cts.Token);
Task winner = await Task.WhenAny(activityTask, timeoutTask);
if (winner == activityTask)
{
// success case
cts.Cancel();
return true;
}
else
{
// timeout case
return false;
}
}
}[!NOTE] The previous C# examples are for Durable Functions 2.x. For Durable Functions 1.x, you must use
DurableOrchestrationContextinstead ofIDurableOrchestrationContext. For more information about the differences between versions, see the Durable Functions versions article.
V3 programming model
const df = require("durable-functions");
const moment = require("moment");
module.exports = df.orchestrator(function*(context) {
const deadline = moment.utc(context.df.currentUtcDateTime).add(30, "s");
const activityTask = context.df.callActivity("FlakyFunction");
const timeoutTask = context.df.createTimer(deadline.toDate());
const winner = yield context.df.Task.any([activityTask, timeoutTask]);
if (winner === activityTask) {
// success case
timeoutTask.cancel();
return true;
} else {
// timeout case
return false;
}
});V4 programming model
const df = require("durable-functions");
const { DateTime } = require("luxon");
df.app.orchestration("timerOrchestrator", function* (context) {
const deadline = DateTime.fromJSDate(context.df.currentUtcDateTime).plus({ seconds: 30 });
const activityTask = context.df.callActivity("FlakyFunction");
const timeoutTask = context.df.createTimer(deadline.toJSDate());
const winner = yield context.df.Task.any([activityTask, timeoutTask]);
if (winner === activityTask) {
// success case
timeoutTask.cancel();
return true;
} else {
// timeout case
return false;
}
});import azure.functions as func
import azure.durable_functions as df
from datetime import datetime, timedelta
def orchestrator_function(context: df.DurableOrchestrationContext):
deadline = context.current_utc_datetime + timedelta(seconds = 30)
activity_task = context.call_activity('FlakyFunction')
timeout_task = context.create_timer(deadline)
winner = yield context.task_any([activity_task, timeout_task])
if winner == activity_task:
timeout_task.cancel()
return True
else:
return False
main = df.Orchestrator.create(orchestrator_function)param($Context)
$expiryTime = New-TimeSpan -Seconds 30
$activityTask = Invoke-DurableActivity -FunctionName 'FlakyFunction' -NoWait
$timerTask = Start-DurableTimer -Duration $expiryTime -NoWait
$winner = Wait-DurableTask -Task @($activityTask, $timerTask) -NoWait
if ($winner -eq $activityTask) {
Stop-DurableTimerTask -Task $timerTask
return $True
}
else {
return $False
}@FunctionName("TimerOrchestrator")
public boolean timerOrchestrator(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
Task<Void> activityTask = ctx.callActivity("FlakyFunction");
Task<Void> timeoutTask = ctx.createTimer(Duration.ofSeconds(30));
Task<?> winner = ctx.anyOf(activityTask, timeoutTask).await();
if (winner == activityTask) {
// success case
return true;
} else {
// timeout case
return false;
}
}Note
This mechanism doesn't end activity function execution that's already in progress. It lets the orchestrator function ignore the result and move on. For more information, see Timers.
::: zone-end
::: zone pivot="durable-task-sdks"
If an activity call takes too long, you can stop waiting for it. Create a durable timer and race it against the activity task.
using Microsoft.DurableTask;
using System;
using System.Threading;
using System.Threading.Tasks;
[DurableTask(nameof(OrchestratorWithTimeout))]
public class OrchestratorWithTimeout : TaskOrchestrator<string, bool>
{
public override async Task<bool> RunAsync(
TaskOrchestrationContext context, string input)
{
TimeSpan timeout = TimeSpan.FromSeconds(30);
DateTime deadline = context.CurrentUtcDateTime.Add(timeout);
using var cts = new CancellationTokenSource();
Task activityTask = context.CallActivityAsync(nameof(SlowActivity), input);
Task timeoutTask = context.CreateTimer(deadline, cts.Token);
Task winner = await Task.WhenAny(activityTask, timeoutTask);
if (winner == activityTask)
{
// Activity completed in time - cancel the timer
cts.Cancel();
return true;
}
else
{
// Timeout occurred
return false;
}
}
}import {
OrchestrationContext,
TOrchestrator,
} from "@microsoft/durabletask-js";
const orchestratorWithTimeout: TOrchestrator = async function* (
ctx: OrchestrationContext
): any {
const timeoutSeconds = 30;
// Start both the activity and a timeout timer
const activityTask = ctx.callActivity(slowActivity, ctx.getInput());
const timeoutTask = ctx.createTimer(timeoutSeconds);
// Wait for whichever completes first
const winner = yield ctx.whenAny([activityTask, timeoutTask]);
if (winner === activityTask) {
// Activity completed in time
return true;
} else {
// Timeout occurred
return false;
}
};import datetime
from durabletask import task
def orchestrator_with_timeout(ctx: task.OrchestrationContext, input_data: str) -> bool:
"""
Orchestrator that demonstrates activity timeout using a timer.
"""
timeout_seconds = 30
deadline = ctx.current_utc_datetime + datetime.timedelta(seconds=timeout_seconds)
# Create both tasks
activity_task = ctx.call_activity("slow_activity", input=input_data)
timeout_task = ctx.create_timer(deadline)
# Wait for whichever completes first
winner = yield task.when_any([activity_task, timeout_task])
if winner == activity_task:
# Activity completed in time
return True
else:
# Timeout occurred
return FalseThis sample is shown for .NET, JavaScript, Java, and Python.
import com.microsoft.durabletask.*;
import java.time.Duration;
public TaskOrchestration createOrchestratorWithTimeout() {
return ctx -> {
String input = ctx.getInput(String.class);
// Create activity task
Task<String> activityTask = ctx.callActivity("SlowActivity", input, String.class);
// Create timeout timer (30 seconds)
Task<Void> timeoutTask = ctx.createTimer(Duration.ofSeconds(30));
// Wait for whichever completes first
Task<?> winner = ctx.anyOf(activityTask, timeoutTask).await();
if (winner == activityTask) {
// Activity completed in time
ctx.complete(true);
} else {
// Timeout occurred
ctx.complete(false);
}
};
}Note
This mechanism doesn't end activity execution that's already in progress. It lets the orchestrator ignore the result and move on. For more information, see the Timers documentation.
::: zone-end
::: zone pivot="durable-functions"
If an orchestrator function fails with an unhandled exception, the runtime logs the exception details, and the instance completes with a Failed status.
In Durable Task workflows that use the .NET Isolated model, task failures are serialized to a FailureDetails object. By default, the object includes these fields:
ErrorType—Exception type nameMessage—Exception messageStackTrace—Serialized stack traceInnerFailure—NestedFailureDetailsobject for inner exceptions
Starting with Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.9.0, you can extend this behavior by implementing IExceptionPropertiesProvider (defined in the Microsoft.DurableTask.Worker package starting in v1.16.1). This provider defines which exception types and properties to include in the FailureDetails.Properties dictionary.
Note
- This feature is available in .NET Isolated only. Support for Java isn't available yet.
- Make sure you're using Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.9.0 or later.
- Make sure you're using Microsoft.DurableTask.Worker v1.16.1 or later.
Implement a custom IExceptionPropertiesProvider to extract and return selected properties for the exceptions you care about. The returned dictionary is serialized to the Properties field of FailureDetails when a matching exception type is thrown.
using Microsoft.DurableTask.Worker;
public class CustomExceptionPropertiesProvider : IExceptionPropertiesProvider
{
public IDictionary<string, object?>? GetExceptionProperties(Exception exception)
{
return exception switch
{
ArgumentOutOfRangeException e => new Dictionary<string, object?>
{
["ParamName"] = e.ParamName,
["ActualValue"] = e.ActualValue
},
InvalidOperationException e => new Dictionary<string, object?>
{
["CustomHint"] = "Invalid operation occurred",
["TimestampUtc"] = DateTime.UtcNow
},
_ => null // Other exception types not handled
};
}
}In Program.cs, register your custom IExceptionPropertiesProvider in your .NET Isolated worker host:
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(builder =>
{
// Register custom exception properties provider
builder.Services.AddSingleton<IExceptionPropertiesProvider, CustomExceptionPropertiesProvider>();
})
.Build();
host.Run();After you register the provider, any exception that matches a handled type automatically includes the configured properties in its FailureDetails.
When an exception occurs that matches your provider’s configuration, the orchestration receives a serialized FailureDetails object like this:
{
"errorType": "TaskFailedException",
"message": "Activity failed with an exception.",
"stackTrace": "...",
"innerFailure": {
"errorType": "ArgumentOutOfRangeException",
"message": "Specified argument was out of range.",
"properties": {
"ParamName": "count",
"ActualValue": 42
}
}
}::: zone-end
::: zone pivot="durable-task-sdks"
If an orchestrator fails because of an unhandled exception, the runtime logs the exception details, and the instance completes with a Failed status. The TaskFailedException has a FailureDetails property that includes the error type, message, and stack trace.
::: zone-end
::: zone pivot="durable-functions"
[!div class="nextstepaction"] Eternal orchestrations
[!div class="nextstepaction"] Diagnose problems
::: zone-end
::: zone pivot="durable-task-sdks"
[!div class="nextstepaction"] Get started with Durable Task SDKs
::: zone-end