#8860·dapr

[Workflows] Introduce `Raise Error` capability (to complement `Raise Event`)

Author: olitomlinsonCreated Jul 8, 2025Updated Sep 29, 2026
Labelskind/enhancementstale

In what area(s)?

/area runtime

Describe the feature

Background

In most languages, errors and exceptions are used to gracefully handle unexpected conditions, and/or for controlling flow.

Dapr Workflows embraces Exception handling with Activities -- If an Activity throws an exception / error, it is marshalled back to the Workflow code, where it can be caught in the Workflow code. Great!

Sadly the capability does not exist when using the Raise Event capability, there is no behavioural equivalent of throwing an Exception / error here, which leads to ugly boiler plate code and inefficient workarounds.

Example 1

In the below example, its trivial to handle the unhappy path of Activities, with Exception handling.

csharp
public override async Task<bool> RunAsync(WorkflowContext context, object state)
{
    try
    {
        var all = new List<Task<bool>>
        {
            context.CallActivityAsync<bool>("foo"),
            context.CallActivityAsync<bool>("bar"),
        };

        await Task.WhenAll(all);
        // happy path processing begins here... yay
    }
    catch (FooException ex)
    {
        // unhappy path
    }
    catch (BarException ex)
    {
        // unhappy path
    }
}   

Example 2 - Include a WaitForExternalEvent

Now let's extend on Example 1 and include a WaitForExternalEvent into the list of tasks we want to wait for.

Sadly the pattern breaks down, we can't simply catch a related Exception / error here, as there is no API to do so.

csharp
public override async Task<bool> RunAsync(WorkflowContext context, object state)
{
    try
    {
        var all = new List<Task<bool>>
        {
            context.CallActivityAsync<bool>("foo"),
            context.CallActivityAsync<bool>("bar"),
            context.WaitForExternalEventAsync<bool>("foobar_event")
        };

        await Task.WhenAll(all);
        // happy path processing begins here... yay
    }
    catch (FooException ex)
    {
        // unhappy path
    }
    catch (BarException ex)
    {
        // unhappy path
    }
    catch (FooBarException ex)
    {
        // THIS IS NOT POSSIBLE
    }
}    

Example 3 - Work-around

  • Added more boilerplate to separate out the tasks results and event results.
  • Added a new event called foobar_error to simulate a first-class error / Exception.
  • Manually convert the foobar_error into a first-class error / Exception
  • As we are now outside of the WhenAny / WhenAll we have no way to react to the other Tasks completing. So we have to introduce a horrific while loop to manually check for the happy path scenario, which is inefficient from a workflow history perspective, and is a subjectively bad dev ex.
csharp
public override async Task<bool> RunAsync(WorkflowContext context, object state)
  {
      try
      {
          var tasks = new List<Task<bool>>();
          var foo = context.CallActivityAsync<bool>("foo");
          var bar = context.CallActivityAsync<bool>("bar");
          tasks.Add(foo);
          tasks.Add(bar);

          var events = new List<Task<bool>>();
          var error = context.WaitForExternalEventAsync<bool>("foobar_error");
          var foobar = context.WaitForExternalEventAsync<bool>("foobar_event");
          events.Add(foobar);
          events.Add(error);

          var anyEvent = Task.WhenAny(events);
          var allTasks = Task.WhenAll(tasks);
          var winner = await Task.WhenAny(anyEvent, allTasks);

          if (error == winner)
          {
              // trigger unhappy path
              throw new FooBarException();
          }

          // at this point we've no idea what tasks have completed yet so we need to go into a manual loop which isn't nice at all
          while (!allTasks.IsCompleted && !foobar.IsCompleted)
          {
              await context.CreateTimer(TimeSpan.FromSeconds(5)); //<---- this sucks
          }

          // happy path processing begins here... yay
      }
      catch (FooException ex)
      {
          // unhappy path
      }
      catch (BarException ex)
      {
          // unhappy path
      }
      catch (FooBarException ex)
      {
          // unhappy path
      }
  }    

Proposal

I believe we need to find a solution to deliver the experience akin to Example 2

I propose that we introduce a complementary first-class Raise Error capability on the Workflow API.

This would allow a caller to utilise Raise Event for the happy path, and Raise Error for the Unhappy path, offering both control flow and exception handling, creating parity with Activities.

csharp
// workflow
var foobar = context.WaitForExternalEventAsync<bool>("foobar_event");

// external system
if (happy)
   workflowClient.RaiseEventAsync("foobar_event", true);
else
   workflowClient.RaiseErrorAsync("This is a custom error message description", ex) // ex is of Type `FooBarException`

Release Note

RELEASE NOTE: