Use await. Ele suspende o método e devolve a thread ao pool enquanto a operação é executada, então a aplicação continua atendendo outros trabalhos. Quando a tarefa falha, a exceção original aparece no ponto em que podemos capturá-la.

Task.Wait() bloqueia a thread que faz a chamada até a tarefa terminar. Ele encapsula qualquer falha em uma AggregateException e, em um ambiente com contexto de sincronização, pode simplesmente entrar em deadlock. O único lugar em que ele se justifica é um ponto de entrada síncrono que não pode ser convertido em async.

Para baixar o código-fonte deste artigo, você pode acessar nosso repositório no GitHub.

await vs Wait: qual é a diferença em C#?

Os dois esperam uma tarefa terminar. A diferença está no que acontece com a thread durante a espera.

await suspende o nosso método e devolve a thread ao pool. O restante do método é registrado como uma continuação e é executado quando a tarefa termina, possivelmente em outra thread. Nada fica bloqueado, então o mesmo pool pequeno de threads continua atendendo outras requisições.

Task.Wait() bloqueia. A thread que fez a chamada fica parada até a tarefa terminar, sem fazer nenhum trabalho útil nesse meio-tempo. Sob carga, é assim que um servidor acaba ficando com latência crescente e CPU ociosa.

As exceções também são diferentes. await relança a exceção original, então um catch (HttpRequestException) comum funciona. Task.Wait() lança uma AggregateException que a encapsula, então o mesmo bloco catch nunca é acionado.

A regra é curta: se o método pode ser async, use await; se não pode, a solução costuma ser torná-lo async, e não bloquear.

Por que o código bloqueante é ruim

Escrevemos código para criar aplicações, e essas aplicações são executadas como comandos dentro de processos do sistema operacional. Cada processo recorre a um número limitado de threads, gerenciadas por um pool de threads, para executá-los.

Todo comando do nosso software é executado em uma thread:

O sistema operacional usa processos para executar as nossas aplicações. Cada processo cria várias threads a partir de um pool de threads

Como esse número tem um teto, o nosso código deve ocupar uma thread pelo menor tempo possível e devolvê-la ao pool assim que ela não for mais necessária.

No entanto, há comandos que, por natureza, levam muito tempo para terminar.

Há 2 categorias dessas operações demoradas:

  • Uso intensivo de CPU: operações pesadas que exigem muito da CPU e bastante tempo para terminar. Exemplos dessas operações são cálculos matemáticos complexos e a compressão de vídeo ou de imagem.
  • Uso intensivo de I/O: operações que dependem muito de recursos externos e remotos e envolvem tempos de espera significativos. Entre os exemplos estão gravar e ler dados no banco de dados e chamar APIs web.

Código bloqueante é o código que reserva uma thread durante todo o período até que uma operação com uso intensivo de CPU ou de I/O termine. Esse período costuma ser longo, e esse tipo de código muitas vezes leva a ineficiências, falta de capacidade de resposta e uma experiência ruim para o usuário.

Como desenvolvedores, devemos evitar escrever código bloqueante. E temos as ferramentas para isso.

Entenda a programação assíncrona por trás de await e Task.Wait

A programação assíncrona é um paradigma de programação que podemos usar para evitar o bloqueio de threads na aplicação. A execução das operações demoradas com uso intensivo de CPU e de I/O passa para um fluxo separado do fluxo principal de execução da aplicação. As threads que esperam essas operações demoradas não ficam bloqueadas: elas são liberadas e usadas em outras tarefas até que a operação demorada termine. Nesse momento, a execução do código continua com a próxima operação. Assim, o sistema continua responsivo, e a percepção geral é de que a aplicação roda mais rápido.

Nas versões mais recentes do .NET, a programação assíncrona é feita com o padrão assíncrono baseado em tarefas e com as palavras-chave await e async.

Você pode ler mais sobre elas no artigo sobre programação assíncrona com async e await no ASP.NET Core, junto com uma análise mais ampla da programação assíncrona.

Agora que aprendemos os fundamentos do código bloqueante e do código assíncrono, vamos ver a seguir os efeitos que Task.Wait() e await têm nas nossas aplicações e como eles se relacionam com os conceitos acima.

Programação assíncrona e Task.Wait

Task é uma classe do namespace System.Threading.Tasks que representa uma operação assíncrona. Quando queremos executar alguma operação de forma assíncrona, podemos encapsulá-la em um objeto Task e iniciá-lo. Task expõe, por meio da sua API pública, muitas funcionalidades úteis que nos permitem interagir com a tarefa e com a operação em execução. Um dos métodos que podemos usar em objetos Task é Task.Wait().

Quando chamamos Task.Wait(), instruímos o runtime a parar e esperar que a Task termine, o que bloqueia a execução. Isso significa que, ao parar, a thread em execução não é liberada. Em vez disso, ela continua ocupada, esperando a operação terminar. Isso também significa que a thread que executa o nosso código antes da chamada a Wait() é a mesma que vai executá-lo depois da chamada. Fica claro que chamar Task.Wait() tem todos os pontos negativos do código bloqueante que mencionamos antes neste artigo.

Colocamos cada método de exemplo em uma classe estática CodeExamples, no seu próprio arquivo CodeExamples.cs, e o chamamos a partir de Program.cs em uma aplicação de console, com await para os assíncronos. Vejamos como chamar o método Task.Wait() em uma Task em execução:

public static void BlockingCodeExample()
{
    Console.WriteLine("** Blocking code Example **");

    Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}");

    Task<string> taskToBlock = Task.Run(() =>
    {
        Thread.Sleep(5000);

        return "Hello, from a blocking task";
    });

    taskToBlock.Wait();

    string blockingResult = taskToBlock.Result;
    Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}");
}

Aqui, criamos o método BlockingCodeExample(), que primeiro imprime no console o CurrentManagedThreadId da thread em execução. Depois, criamos um objeto do tipo Task, que encapsula um delegado Func. O delegado chama Thread.Sleep() para simular uma operação demorada que leva 5 segundos para terminar e retorna uma string.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Em seguida, chamamos o método Wait() em Task, o que faz a thread em execução ficar bloqueada enquanto espera a conclusão da operação demorada. Depois disso, acessamos a propriedade Result de taskToBlock para obter o resultado da tarefa.

Por fim, imprimimos de novo o CurrentManagedThreadId e vemos que a thread é a mesma.

Vejamos a saída no console:

** Blocking code Example **
Current ManagedThreadId: 2
Current ManagedThreadId: 2

Vamos continuar com o operador await para ver como ele difere do que vimos até agora.

Programação assíncrona e await

O operador await serve para esperar de forma assíncrona que uma operação termine. Assim que o runtime percebe que uma Task está sendo aguardada, a execução para e a thread em execução é liberada até que a operação assíncrona termine. Até isso acontecer, o runtime pode usar a thread em outras operações, e o sistema continua responsivo.

Além disso, a palavra-chave await “desembrulha” o valor retornado pela operação assíncrona quando ela termina e nos permite atribuí-lo a uma variável.

O operador await só pode ser usado em métodos assíncronos marcados com o modificador async. Também é uma boa prática que o método tenha tipo de retorno Task ou Task<T>. Assim, quem chama o método também pode usar await. Já os manipuladores de eventos podem ser marcados como async com tipo de retorno void.

Agora vejamos como usar a palavra-chave await em um método assíncrono:

public async static Task AsynchronousCodeExampleAsync()
{
    Console.WriteLine("** Asynchronous code Example **");

    Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}");

    Task<string> taskToAwait = Task.Run(() =>
    {
        Thread.Sleep(5000);

        return "Hello, from an asyncrhonous task";
    });

    string awaitResult = await taskToAwait;

    Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}");
}

Aqui, declaramos o método AsynchronousCodeExampleAsync() e o marcamos como async para poder usar o operador await dentro dele. Imprimimos o CurrentManagedThreadId da thread em execução e criamos a Task que vamos aguardar, simulando uma operação demorada com uma chamada a Thread.Sleep().

Em seguida, usamos a palavra-chave await para esperar de forma assíncrona a conclusão da Task e receber o resultado dela. Observe que, neste caso, não precisamos usar a propriedade Result, como fizemos antes. Por fim, imprimimos de novo o CurrentManagedThreadId.

Vejamos a saída do método:

** Asynchronous code Example **
Current ManagedThreadId: 2
Current ManagedThreadId: 10

Observe que, neste caso, CurrentManagedThreadId mudou de valor depois do uso da palavra-chave await. Isso acontece porque o runtime decide trocar a thread em execução. Como mencionamos antes, o await não bloqueia a thread inicial enquanto espera a conclusão. Dependendo do tipo de aplicação e do nosso código, o runtime pode esperar que a thread inicial fique livre de novo para continuar a execução depois que a operação demorada terminar.

Os dois exemplos acima usam Thread.Sleep() apenas como substituto de um trabalho real; veja Thread.Sleep vs Task.Delay para entender por que a escolha entre os dois importa quando a espera é real. Essa distinção também não se confunde com a de retornar ou aguardar uma tarefa de dentro de um método, que trata do que o próprio método retorna, e não do que ele espera.

Como esperar uma tarefa em C# sem bloquear?

Esperar sem bloquear significa aguardar com await, e a forma da espera depende do que estamos esperando.

Para uma tarefa, await task. Para várias que podem ser executadas ao mesmo tempo, await Task.WhenAll(t1, t2) aguarda todas juntas e continua quando a última termina, muito mais rápido do que executar uma de cada vez.

Para o primeiro resultado entre várias, await Task.WhenAny(tasks) retorna a tarefa que terminou primeiro, e é assim que se expressa um tempo limite sem bloquear nada.

Para esperar por um período fixo, await Task.Delay(ms), nunca Thread.Sleep(ms): o primeiro libera a thread, o segundo a mantém ocupada.

Duas formas parecem uma espera sem bloqueio, mas não são. task.Result e task.GetAwaiter().GetResult() bloqueiam, e a segunda só difere por desembrulhar a exceção; nenhuma das duas é uma espera sem bloqueio.

Se o método não pode ser marcado como async porque a assinatura de uma interface ou do framework não permite, essa assinatura é o problema que vale a pena corrigir.

Task.WhenAll e Task.WhenAny são o ponto de partida para executar várias tarefas de forma assíncrona. Quando realmente não há como tornar a assinatura assíncrona, o artigo sobre como executar um método assíncrono de forma síncrona trata dos poucos casos em que essa é a única opção.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Tratamento de exceções com await e Task.Wait

Agora que discutimos as diferenças entre await e Task.Wait() no fluxo de execução, vejamos também como eles diferem no tratamento de exceções.

Como vimos, a palavra-chave await retorna o resultado de uma operação assíncrona quando ela termina. Além disso, verifica se alguma Exception foi lançada dentro da operação assíncrona e lança a Exception de volta para o método que está aguardando. Por isso, a experiência que temos é muito parecida com a da execução síncrona.

Agora vejamos isso na prática:

public async static Task AsynchronousExceptionHandlingAsync()
{
   Console.WriteLine("** Asynchronous Exception Handling **");

    try
    {
        Task taskToFail = Task.Run(() =>
        {
            throw new ApplicationException("Error in the asynchronous long-running operation");
        });

        await taskToFail;
    }
    catch (ApplicationException ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Aqui, definimos o método AsynchronousExceptionHandlingAsync(), que executa uma Task que lança uma ApplicationException. Usamos um bloco try-catch para envolver o código e tratar qualquer exceção.

Agora, vamos executar esse código; devemos ver que capturamos a ApplicationException com sucesso:

** Asynchronous Exception Handling **
Error in the asynchronous long-running operation

Agora vejamos como o mesmo código se comporta quando usamos Task.Wait():

public static void BlockingExceptionHandling()
{
    Console.WriteLine("** Blocking Exception Handling **");

    try
    {
        Task taskToFail = Task.Run(() =>
        {
            throw new ApplicationException("Error in the blocking long-running operation");
        });

        taskToFail.Wait();
    }
    catch (ApplicationException ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Aqui, definimos o método BlockingExceptionHandling(), que executa uma Task que lança uma ApplicationException. A única diferença desta vez é que não aguardamos o resultado com await e, em vez disso, chamamos o método Wait().

Executamos esse código, mas percebemos que ele quebra!

Vejamos a saída:

Mostra a exceção lançada pelo tratamento de exceções com bloqueio.

Parece que o nosso código lança uma System.AggregateException em vez da ApplicationException que esperávamos. A programação baseada em tarefas usa a AggregateException para consolidar várias exceções que tarefas paralelas podem lançar. É um conceito poderoso quando lidamos com tarefas concorrentes e queremos verificar o status delas e relatar as falhas. No entanto, ao tratar erros em uma única tarefa assíncrona, a AggregateException acrescenta complexidade ao código sem nenhum benefício.

Por que Task.Wait() causa deadlocks?

O deadlock acontece quando a thread bloqueada é a única autorizada a concluir o trabalho que ela mesma está esperando.

Alguns ambientes instalam um contexto de sincronização que permite que apenas uma thread por vez execute continuações: a thread da interface do usuário no WinForms e no WPF, a thread que detém o contexto da requisição no ASP.NET clássico. Chamar Task.Wait() ali bloqueia essa thread.

A operação aguardada termina, a continuação dela é enfileirada nesse mesmo contexto e a thread não consegue executá-la porque continua bloqueada, esperando. Os dois lados esperam para sempre.

O ASP.NET Core removeu o contexto de sincronização da requisição, então o travamento clássico é mais raro do que era. Ele não desapareceu: o WPF, o WinForms e qualquer contexto personalizado de thread única ainda o reproduzem.

A solução de contorno que as pessoas costumam adotar é usar ConfigureAwait(false) em todas as chamadas da cadeia, o que só funciona se nenhuma chamada tiver sido esquecida.

Stephen Toub, da equipe do .NET, explica a diferença com clareza no ConfigureAwait FAQ: “enquanto o ASP.NET clássico no .NET Framework tem o seu próprio SynchronizationContext, o ASP.NET Core, ao contrário, não tem”.

Critérioawait tasktask.Wait() / task.Result
Ocupa a thread que faz a chamadaNão: ela volta ao poolSim: fica bloqueada até a conclusão
Pode ser usado em um método síncronoNão: o método precisa ser asyncSim
Exceção recebidaA exceção originalUma AggregateException que a encapsula
O try/catch captura o tipo realSimSó por meio de ex.InnerException
Risco de deadlock com contexto de sincronizaçãoNenhumReal: o ASP.NET clássico e o WinForms/WPF travam
CancelamentoCancellationToken na chamada aguardadaWait(timeout) retorna, mas o trabalho continua em execução
Custo no pool de threads sob cargaUma thread atende muitas esperasUma thread bloqueada por espera
Onde usarEm todo lugarMain antes do C# 7.1 e construtores

A tabela também corresponde à posição da Microsoft: as orientações dela sobre programação assíncrona trazem uma tabela “Código atual → Substituir por ‘await'” que mapeia Task.Wait ou Task.Result para await, Task.WaitAny para await Task.WhenAny, Task.WaitAll para await Task.WhenAll e Thread.Sleep para await Task.Delay (Asynchronous programming scenarios, Microsoft Learn, consultado em 9 de agosto de 2026).

Conclusão

Neste artigo, analisamos as diferenças entre o operador await e o método Task.Wait(). Começamos discutindo o que é código bloqueante, por que ele é uma má prática e como podemos usar código assíncrono no lugar dele. Vimos como usar await e Task.Wait() no nosso código e como eles afetam o fluxo de execução dos nossos programas. Vimos também como o await facilita o tratamento de exceções e por que devemos usá-lo para criar sistemas eficientes e responsivos.

Testado com o .NET 10.0.10.