Uma boa solução para esperar em try / catch / finally?

92

Preciso chamar um asyncmétodo em um catchbloco antes de lançar novamente a exceção (com seu rastreamento de pilha) como este:

try
{
    // Do something
}
catch
{
    // <- Clean things here with async methods
    throw;
}

Mas, infelizmente, você não pode usar awaitem um bloco catchou finally. Aprendi que é porque o compilador não tem como voltar em um catchbloco para executar o que está depois de sua awaitinstrução ou algo assim ...

Tentei usar Task.Wait()para substituir awaite obtive um impasse. Pesquisei na Web como poderia evitar isso e encontrei este site .

Como não posso alterar os asyncmétodos nem sei se eles usam ConfigureAwait(false), criei esses métodos que usam um Func<Task>que inicia um método assíncrono quando estamos em um thread diferente (para evitar um deadlock) e aguarda sua conclusão:

public static void AwaitTaskSync(Func<Task> action)
{
    Task.Run(async () => await action().ConfigureAwait(false)).Wait();
}

public static TResult AwaitTaskSync<TResult>(Func<Task<TResult>> action)
{
    return Task.Run(async () => await action().ConfigureAwait(false)).Result;
}

public static void AwaitSync(Func<IAsyncAction> action)
{
    AwaitTaskSync(() => action().AsTask());
}

public static TResult AwaitSync<TResult>(Func<IAsyncOperation<TResult>> action)
{
    return AwaitTaskSync(() => action().AsTask());
}

Portanto, minhas perguntas são: Você acha que este código está correto?

Claro, se você tiver algumas melhorias ou conhecer uma abordagem melhor, estou ouvindo! :)

user2397050
fonte
2
O uso awaitem um bloco catch é permitido desde C # 6.0 (veja minha resposta abaixo)
Adi Lester
3
Mensagens de erro C # 5.0 relacionadas : CS1985 : Não é possível aguardar no corpo de uma cláusula catch. CS1984 : Não é possível esperar no corpo de uma cláusula finally.
DavidRR

Respostas:

172

Você pode mover a lógica para fora do catchbloco e relançar a exceção depois, se necessário, usando ExceptionDispatchInfo.

static async Task f()
{
    ExceptionDispatchInfo capturedException = null;
    try
    {
        await TaskThatFails();
    }
    catch (MyException ex)
    {
        capturedException = ExceptionDispatchInfo.Capture(ex);
    }

    if (capturedException != null)
    {
        await ExceptionHandler();

        capturedException.Throw();
    }
}

Dessa forma, quando o chamador inspeciona a StackTracepropriedade da exceção , ele ainda registra onde TaskThatFailsela foi lançada.

Sam Harwell
fonte
1
Qual é a vantagem de manter em ExceptionDispatchInfovez de Exception(como na resposta de Stephen Cleary)?
Varvara Kalinina
2
Eu posso supor que se você decidir relançar o Exception, você perderá todo o anterior StackTrace?
Varvara Kalinina
1
@VarvaraKalinina Exatamente.
54

Você deve saber que, desde o C # 6.0, é possível usar blocos awaitin catche finally, portanto, você pode fazer o seguinte:

try
{
    // Do something
}
catch (Exception ex)
{
    await DoCleanupAsync();
    throw;
}

Os novos recursos do C # 6.0, incluindo o que acabei de mencionar, estão listados aqui ou como um vídeo aqui .

Adi Lester
fonte
Suporte para await em blocos catch / finally em C # 6.0 também é indicado na Wikipedia.
DavidRR
4
@DavidRR. a Wikipedia não é autorizada. É apenas mais um site entre milhões, no que diz respeito a isso.
user34660
Embora isso seja válido para C # 6, a pergunta foi marcada como C # 5 desde o início. Isso me faz pensar se essa resposta aqui é confusa ou se devemos apenas remover a tag de versão específica nesses casos.
julealgon
16

Se você precisar usar asyncgerenciadores de erros, recomendo algo assim:

Exception exception = null;
try
{
  ...
}
catch (Exception ex)
{
  exception = ex;
}

if (exception != null)
{
  ...
}

O problema com o bloqueio síncrono no asynccódigo (independentemente do thread em que esteja sendo executado) é que você está bloqueando de forma síncrona. Na maioria dos cenários, é melhor usar await.

Atualização: Já que você precisa relançar, você pode usar ExceptionDispatchInfo.

Stephen Cleary
fonte
1
Obrigado, mas infelizmente já conheço esse método. Isso é o que eu faço normalmente, mas não posso fazer isso aqui. Se eu simplesmente usar throw exception;na ifinstrução, o rastreamento da pilha será perdido.
user2397050
3

Extraímos grande resposta do hvd para a seguinte classe de utilitário reutilizável em nosso projeto:

public static class TryWithAwaitInCatch
{
    public static async Task ExecuteAndHandleErrorAsync(Func<Task> actionAsync,
        Func<Exception, Task<bool>> errorHandlerAsync)
    {
        ExceptionDispatchInfo capturedException = null;
        try
        {
            await actionAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            capturedException = ExceptionDispatchInfo.Capture(ex);
        }

        if (capturedException != null)
        {
            bool needsThrow = await errorHandlerAsync(capturedException.SourceException).ConfigureAwait(false);
            if (needsThrow)
            {
                capturedException.Throw();
            }
        }
    }
}

Seria usado da seguinte maneira:

    public async Task OnDoSomething()
    {
        await TryWithAwaitInCatch.ExecuteAndHandleErrorAsync(
            async () => await DoSomethingAsync(),
            async (ex) => { await ShowMessageAsync("Error: " + ex.Message); return false; }
        );
    }

Sinta-se à vontade para melhorar a nomenclatura, nós o mantivemos intencionalmente prolixo. Observe que não há necessidade de capturar o contexto dentro do wrapper, pois ele já está capturado no site da chamada, portanto ConfigureAwait(false).

mrts
fonte