Dušan
Dušan

Reputation: 364

How to handle errors in method that has IAsyncEnumerable as return type

I have an API endpoint:

[HttpGet("api/query")]
public async IAsyncEnumerable<dynamic> Query(string name)
{    
   await foreach(var item in _myService.CallSomethingReturningAsyncStream(name))
   {
       yield return item;
   }
}

I would like to be able to in case of an ArgumentException return something like "Bad request" response.

If I try using try-catch block, I get error:

CS1626: Cannot yield a value in the body of a try block with a catch clause

Please note that it is an API endpoint method, so error handling should ideally be in the same method, without need for making additional middlewares.

If needed, rough implementation of CallSomethingReturningAsyncStream method:

public async IAsyncEnumerable<dynamic> CallSomethingReturningAsyncStream(string name)
{
   if (string.IsNullOrWhiteSpace(name))
      throw new ArgumentException("Name is missing", nameof(name));

   (...)

   foreach (var item in result)
   {
       yield return item;
   }
}

Upvotes: 3

Views: 3950

Answers (2)

Evk
Evk

Reputation: 101483

You have to split method. Extract part which does async processing:

private async IAsyncEnumerable<dynamic> ProcessData(TypeOfYourData data)
{    
   await foreach(var item in data)
   {
       yield return item;
   }
}

And then in API method do:

[HttpGet("api/query")]
public IActionResult Query(string name)
{    
   TypeOfYourData data;
   try {
       data = _myService.CallSomethingReturningAsyncStream(name);
   }
   catch (...) {
        // do what you need
        return BadRequest();
   }
   return Ok(ProcessData(data));
}

Or actually you can just move the whole thing into separate method:

[HttpGet("api/query")]
public IActionResult Query(string name)
{           
   try {
       return Ok(TheMethodYouMovedYourCurrentCodeTo);
   }
   catch (...) {
        // do what you need
        return BadRequest();
   }
}

It will of course only catch exceptions thrown before actual async enumeration starts, but that's fine for your use case as I understand. Returning bad request after async enumeration has started is not possible, because response is already being sent to client.

Update: you mentioned your service code is:

public async IAsyncEnumerable<dynamic> CallSomethingReturningAsyncStream(string name)
{
   if (string.IsNullOrWhiteSpace(name))
      throw new ArgumentException("Name is missing", nameof(name));


   foreach (var item in result)
   {
       yield return item;
   }
}

It has the same problem as your current api method. It's generally a good practice to split such methods in two - first part performs validation, and then returns the second part. For example let's consider this code:

public class Program {
    static void Main() {
        try {
            CallSomethingReturningAsyncStream(null);
        }
        catch (Exception ex) {
            Console.WriteLine(ex);
        }
    }

    public static async IAsyncEnumerable<string> CallSomethingReturningAsyncStream(string name) {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Name is missing", nameof(name));

        await Task.Delay(100);
        yield return "1";
    }
}

It will complete without any exceptions, because the code inside CallSomethingReturningAsyncStream was converted by compiler into a state machine and will only execute when you enumerate the result, including part which validates arguments.

However, if you change your method like this:

public class Program {
    static void Main() {
        try {
            CallSomethingReturningAsyncStream(null);
        }
        catch (Exception ex) {
            Console.WriteLine(ex);
        }
    }

    public static IAsyncEnumerable<string> CallSomethingReturningAsyncStream(string name) {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Name is missing", nameof(name));

        return CallSomethingReturningAsyncStreamInternal(name);
    }
    
    // you can also put that inside method above,
    // as local function
    private static async IAsyncEnumerable<string> CallSomethingReturningAsyncStreamInternal(string name) {
        await Task.Delay(100);
        yield return "1";
    }
}

It will do the same thing, BUT now method validates arguments synchronously, and then goes to async part. This code will throw argument exception which will be caught.

So I'd suggest to follow this best practice for your code, and that will also resolve your issue stated in question. There is no reason to delay throwing exception until enumeration occurs when it's clear right away that arguments are invalid and enumeration is not possible anyway.

Upvotes: 1

Theodor Zoulias
Theodor Zoulias

Reputation: 43545

You could install the System.Interactive.Async package, and do this:

[HttpGet("api/query")]
public IAsyncEnumerable<dynamic> Query(string name)
{
    return AsyncEnumerableEx.Defer(() => _myService.CallSomethingReturningAsyncStream(name))
        .Catch<dynamic, Exception>(ex =>
            AsyncEnumerableEx.Return<dynamic>($"Bad request: {ex.Message}"));
}

The signature of the Defer operator:

// Returns an async-enumerable sequence that invokes the specified
// factory function whenever a new observer subscribes.
public static IAsyncEnumerable<TSource> Defer<TSource>(
    Func<IAsyncEnumerable<TSource>> factory)

The signature of the Catch operator:

// Continues an async-enumerable sequence that is terminated by
// an exception of the specified type with the async-enumerable sequence
// produced by the handler.
public static IAsyncEnumerable<TSource> Catch<TSource, TException>(
    this IAsyncEnumerable<TSource> source,
    Func<TException, IAsyncEnumerable<TSource>> handler);

The signature of the Return operator:

// Returns an async-enumerable sequence that contains a single element.
public static IAsyncEnumerable<TValue> Return<TValue>(TValue value)

The Defer might seem superficial, but it is needed for the case that the _myService.CallSomethingReturningAsyncStream throws synchronously. In case this method is implemented as an async iterator, it will never throw synchronously, so you could omit the Defer.

Upvotes: 2

Related Questions