Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

await forbidden in catch clause. Looking for a work arround

I know that await can't be used in catch clause. However I haven't really faced a problem related to this, until now ...

Basically I have a layer that is responsible for receiving incoming requests, processing them, building messages from them and pass the messages to another layer responsible to send the messages.

If something goes wrong during the sending of the message, a custom exception is thrown, and caught by the message sending layer. At this point, a failure record for this message should be inserted in DB (takes some time, so async), and the exception should be propagated to the upper layer which is in charge of sending an error response to the client that made the request.

Here is some very simplified code for illustration purpose below :

public async Task ProcessRequestAsync(Request req)
{
    int statusCode = 0;

    try
    {       
       await SendMessageAsync(new Message(req));        
    }
    catch(MyCustomException ex)
    {
       statusCode = ex.ErrorCode;
    }

    await SendReponseToClientAsync(req, statusCode);
}


public async Task SendMessageAsync(Message msg)
{
    try
    {           
       // Some async operations done here
    }
    catch (MyCustomException ex)
    {       
        await InsertFailureForMessageInDbAsync(msg, ex.ErrorCode); // CAN'T DO THAT
        throw;
    }
}

Of course the request processing layer knows nothing about the DB, it is just in charge of building a message, passing the message to the message processing layer, and send a response to the client (positive or negative).

So I think it makes sense ... If an exception happens, my "business" layer want to insert a failure record in DB and rethrow the exception so that the "request" processing layer can do what's necessary (in this context, send a negative response back to the client).

Should exceptions not be used this way ? It seems clean to me but the fact that I can't do this await in the catch clause makes me think there is maybe a code smell with the design (even if I the idea of handling the exception in one layer then rethrowing it for the upper layer to do some different handling as well sounds to me like it's exactly what exceptions are made for).

Any idea arround this ?

Thanks !

like image 489
darkey Avatar asked Dec 15 '12 01:12

darkey


People also ask

Can you await in a catch block?

C# await is a keyword. It is used to suspend execution of the method until the awaited task is complete. In C# 6.0, Microsoft added a new feature that allows us to use await inside the catch or finally block. So, we can perform asynchronous tasks while exception is occurred.

What happens if you don't await a task?

If you don't await the task or explicitly check for exceptions, the exception is lost. If you await the task, its exception is rethrown. As a best practice, you should always await the call. By default, this message is a warning.


1 Answers

I've run into this a couple of times as well.

As Rafael commented, you could just ignore the result of InsertFailureForMessageInDbAsync:

public async Task SendMessageAsync(Message msg)
{
  try
  {           
    // Some async operations done here
  }
  catch (MyCustomException ex)
  {       
    var _ = InsertFailureForMessageInDbAsync(msg, ex.ErrorCode);
    throw;
  }
}

Note that any exceptions from InsertFailureForMessageInDbAsync will be ignored by default.

Your other option is more complex:

public async Task DoSendMessageAsync(Message msg)
{
  // Some async operations done here
}

public async Task SendMessageAsync(Message msg)
{
  var task = DoSendMessageAsync(msg);
  MyCustomException exception = null;
  try
  {
    await task;
    return;
  }
  catch (MyCustomException ex)
  {
    exception = ex;
  }

  await Task.WhenAll(task, InsertFailureForMessageInDbAsync(msg, exception.ErrorCode));
}

This will asynchronously handle the exception and return a Task that has a real AggregateExption (containing both exceptions if InsertFailureForMessageInDbAsync does throw one).

Unfortunately, await will ignore the second exception. If you really want all exceptions passed up, you can replace the last line (await Task.WhenAll...) with something like this:

Exception exception2 = null;
try
{
  await InsertFailureForMessageInDbAsync(msg, exception.ErrorCode);
}
catch (Exception ex)
{
  exception2 = ex;
}

if (exception2 == null)
  throw new AggregateException(exception);
else
  throw new AggregateException(exception, exception2);

But that's pretty complex, and not exactly the kind of pattern you want repeated. If possible, I'd just ignore the logging result as Rafael recommended.

like image 53
Stephen Cleary Avatar answered Sep 22 '22 05:09

Stephen Cleary