diff --git a/src/Abstractions/SubOrchestrationFailedException.cs b/src/Abstractions/SubOrchestrationFailedException.cs
new file mode 100644
index 000000000..7bc12d82a
--- /dev/null
+++ b/src/Abstractions/SubOrchestrationFailedException.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+using CoreOrchestrationException = DurableTask.Core.Exceptions.OrchestrationException;
+
+namespace Microsoft.DurableTask;
+
+///
+/// Exception that gets thrown when a durable task sub-orchestration, fails with an
+/// unhandled exception.
+///
+///
+/// Detailed information associated with a particular task failure, including exception details, can be found in the
+/// property.
+///
+public sealed class SubOrchestrationFailedException : Exception
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The failed sub-orchestrationname.
+ /// The task ID.
+ /// The failure details.
+ public SubOrchestrationFailedException(string taskName, int taskId, TaskFailureDetails failureDetails)
+ : base(GetExceptionMessage(taskName, taskId, failureDetails, null))
+ {
+ this.TaskName = taskName;
+ this.TaskId = taskId;
+ this.FailureDetails = failureDetails;
+ }
+
+ ///
+ /// Gets the name of the failed sub-orchestration.
+ ///
+ public string TaskName { get; }
+
+ ///
+ /// Gets the ID of the failed sub-orchestration.
+ ///
+ ///
+ /// Each durable task (activities, timers, sub-orchestrations, etc.) scheduled by a task orchestrator has an
+ /// auto-incrementing ID associated with it. This ID is used to distinguish tasks from one another, even if, for
+ /// example, they are tasks that call the same activity. This ID can therefore be used to more easily correlate a
+ /// specific task failure to a specific task.
+ ///
+ public int TaskId { get; }
+
+ ///
+ /// Gets the details of the task failure, including exception information.
+ ///
+ public TaskFailureDetails FailureDetails { get; }
+
+ // This method is the same as the one in `TaskFailedException` to keep the exception message format consistent.
+ static string GetExceptionMessage(string taskName, int taskId, TaskFailureDetails? details, Exception? cause)
+ {
+ // NOTE: Some integration tests depend on the format of this exception message.
+ string? subMessage = null;
+ if (details is not null)
+ {
+ subMessage = details.ErrorMessage;
+ }
+ else if (cause is global::DurableTask.Core.Exceptions.OrchestrationException coreEx)
+ {
+ subMessage = coreEx.FailureDetails?.ErrorMessage;
+ }
+
+ if (subMessage is null)
+ {
+ subMessage = cause?.Message;
+ }
+
+ return subMessage is null
+ ? $"Task '{taskName}' (#{taskId}) failed with an unhandled exception."
+ : $"Task '{taskName}' (#{taskId}) failed with an unhandled exception: {subMessage}";
+ }
+}
diff --git a/src/Abstractions/TaskFailedException.cs b/src/Abstractions/TaskFailedException.cs
index 18e0addf3..8362c02b7 100644
--- a/src/Abstractions/TaskFailedException.cs
+++ b/src/Abstractions/TaskFailedException.cs
@@ -4,7 +4,7 @@
namespace Microsoft.DurableTask;
///
-/// Exception that gets thrown when a durable task, such as an activity or a sub-orchestration, fails with an
+/// Exception that gets thrown when a durable task, such as an activity , fails with an
/// unhandled exception.
///
///
diff --git a/src/Abstractions/TaskOrchestrationContext.cs b/src/Abstractions/TaskOrchestrationContext.cs
index ecba913be..0c6fb58bc 100644
--- a/src/Abstractions/TaskOrchestrationContext.cs
+++ b/src/Abstractions/TaskOrchestrationContext.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using Microsoft.DurableTask.Abstractions;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Logging;
@@ -332,9 +333,9 @@ public virtual Task CallSubOrchestratorAsync(TaskName orchestratorName, TaskOpti
///
/// Thrown if the calling thread is anything other than the main orchestrator thread.
///
- ///
+ ///
/// The sub-orchestration failed with an unhandled exception. The details of the failure can be found in the
- /// property.
+ /// property.
///
public virtual Task CallSubOrchestratorAsync(
TaskName orchestratorName, object? input = null, TaskOptions? options = null)
diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
index 8ce7d5125..bf0434216 100644
--- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
+++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
@@ -7,6 +7,7 @@
using DurableTask.Core;
using DurableTask.Core.Entities.OperationFormat;
using DurableTask.Core.Serializing.Internal;
+using Microsoft.DurableTask.Abstractions;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Logging;
@@ -197,8 +198,8 @@ public override async Task CallSubOrchestratorAsync(
}
catch (global::DurableTask.Core.Exceptions.SubOrchestrationFailedException e)
{
- // Hide the core DTFx types and instead use our own
- throw new TaskFailedException(
+ // Hide the core DTFx types and instead use our own SubOrchestrationFailedException
+ throw new SubOrchestrationFailedException(
orchestratorName,
e.ScheduleId,
TaskFailureDetails.FromCoreFailureDetails(e.FailureDetails!));
diff --git a/src/Worker/Core/Shims/TaskOrchestrationShim.cs b/src/Worker/Core/Shims/TaskOrchestrationShim.cs
index d3fb7aa36..17daef523 100644
--- a/src/Worker/Core/Shims/TaskOrchestrationShim.cs
+++ b/src/Worker/Core/Shims/TaskOrchestrationShim.cs
@@ -64,6 +64,16 @@ public TaskOrchestrationShim(
FailureDetails = new FailureDetails(e, e.FailureDetails.ToCoreFailureDetails()),
};
}
+ catch (SubOrchestrationFailedException e)
+ {
+ // Convert back to something the Durable Task Framework natively understands so that
+ // failure details are correctly propagated.
+ // This has to be DurableTask.Core.TaskFailedException instead of DurableTask.Core.SubOrchestratorFailedException because of core logic.
+ throw new CoreTaskFailedException(e.Message, e.InnerException)
+ {
+ FailureDetails = new FailureDetails(e, e.FailureDetails.ToCoreFailureDetails()),
+ };
+ }
finally
{
// if user code crashed inside a critical section, or did not exit it, do that now
diff --git a/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs b/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs
index c8fb31181..0b2a44813 100644
--- a/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs
+++ b/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs
@@ -7,6 +7,7 @@
using Xunit.Abstractions;
using Xunit.Sdk;
+
namespace Microsoft.DurableTask.Grpc.Tests;
///
@@ -327,8 +328,8 @@ public async Task RetrySubOrchestrationFailures(int expectedNumberOfAttempts, Ty
Assert.NotNull(metadata.FailureDetails);
Assert.Contains(errorMessage, metadata.FailureDetails!.ErrorMessage);
- // The root orchestration failed due to a failure with the sub-orchestration, resulting in a TaskFailedException
- Assert.True(metadata.FailureDetails.IsCausedBy());
+ // The root orchestration failed due to a failure with the sub-orchestration, resulting in a Microsoft.DurableTask.SubOrchestrationFailedException
+ Assert.True(metadata.FailureDetails.IsCausedBy());
}
[Theory]
@@ -391,11 +392,11 @@ public async Task RetrySubOrchestratorFailuresCustomLogicAndPolicy(
//Assert.Equal(expectedNumberOfAttempts, retryHandlerCalls);
Assert.Equal(expectedNumberOfAttempts, actualNumberOfAttempts);
- // The root orchestration failed due to a failure with the sub-orchestration, resulting in a TaskFailedException
+ // The root orchestration failed due to a failure with the sub-orchestration, resulting in a Microsoft.DurableTask.SubOrchestrationFailedException
if (expRuntimeStatus == OrchestrationRuntimeStatus.Failed)
{
Assert.NotNull(metadata.FailureDetails);
- Assert.True(metadata.FailureDetails!.IsCausedBy());
+ Assert.True(metadata.FailureDetails!.IsCausedBy());
}
else
{
@@ -463,9 +464,9 @@ public async Task RetrySubOrchestratorFailuresCustomLogic(int expectedNumberOfAt
Assert.Equal(expectedNumberOfAttempts, retryHandlerCalls);
Assert.Equal(expectedNumberOfAttempts, actualNumberOfAttempts);
- // The root orchestration failed due to a failure with the sub-orchestration, resulting in a TaskFailedException
+ // The root orchestration failed due to a failure with the sub-orchestration, resulting in a SubOrchestrationFailedException
Assert.NotNull(metadata.FailureDetails);
- Assert.True(metadata.FailureDetails!.IsCausedBy());
+ Assert.True(metadata.FailureDetails!.IsCausedBy());
}
[Theory]
@@ -539,7 +540,7 @@ static void ValidateInnermostFailureDetailsChain(TaskFailureDetails? failureDeta
{
await ctx.CallSubOrchestratorAsync("Sub");
}
- catch (TaskFailedException ex)
+ catch (SubOrchestrationFailedException ex)
{
// Outer failure represents the orchestration failure
Assert.NotNull(ex.FailureDetails);
@@ -580,7 +581,7 @@ static void ValidateInnermostFailureDetailsChain(TaskFailureDetails? failureDeta
// Check to make sure that the wrapper failure details exist as expected
Assert.NotNull(metadata.FailureDetails);
- Assert.True(metadata.FailureDetails!.IsCausedBy());
+ Assert.True(metadata.FailureDetails!.IsCausedBy());
Assert.Contains("Sub", metadata.FailureDetails.ErrorMessage);
Assert.NotNull(metadata.FailureDetails.InnerFailure);
Assert.True(metadata.FailureDetails.InnerFailure!.IsCausedBy());