1. A boa notícia: a sintaxe já é familiar
async function obterCliente(id) {
const resposta = await fetch(`/clientes/${id}`);
const cliente = await resposta.json();
return cliente;
}
async Task<Cliente> ObterClienteAsync(int id)
{
var resposta = await httpClient.GetAsync($"/clientes/{id}");
var cliente = await resposta.Content.ReadFromJsonAsync<Cliente>();
return cliente!;
}
Três diferenças de sintaxe, todas superficiais:
| JavaScript | C# |
|---|---|
Promise<T> (implícito no retorno) | Task<T> explícito no tipo de retorno |
| Nome do método sem convenção fixa | Convenção: sufixo Async no nome (ObterClienteAsync) |
Promise.all([...]) | await Task.WhenAll(...) |
2. Task vs. Task<T> vs. void
| Tipo de retorno | Equivalente JS | Quando usar |
|---|---|---|
Task | Promise<void> | Método assíncrono que não devolve valor |
Task<T> | Promise<T> | Método assíncrono que devolve um valor do tipo T |
async void | — | Evite — só é aceitável em handlers de evento (ex: clique de botão em UI) |
async void não pode ser aguardado (await)
por quem chama, e uma exceção lançada dentro dele derruba o processo em vez de propagar
normalmente. Fora do caso específico de handlers de evento em UI, sempre use
async Task ou async Task<T>.
3. O que realmente muda: threads, não sintaxe
Node.js é single-threaded — async/await ali existe para não bloquear
o único loop de eventos enquanto se espera I/O. C#/.NET é multi-thread por natureza, e
async/await existe para não bloquear a thread atual
(que pode ser uma entre várias) enquanto se espera uma operação — a thread fica livre para fazer
outro trabalho nesse meio tempo, e retoma quando a operação awaited terminar.
Fig. 1 — await libera a thread durante a espera; ela volta a ser usada assim que a operação termina.
4. ConfigureAwait e o contexto de sincronização
Em código de biblioteca (não em endpoints de API ASP.NET Core), é comum ver
.ConfigureAwait(false) depois de um await.
Isso diz ao runtime "não precisa retomar necessariamente na mesma thread/contexto de origem" —
uma otimização que evita deadlocks em certos cenários mais antigos (WPF/WinForms, capítulo do
Módulo 6). Em APIs ASP.NET Core modernas isso não é mais estritamente necessário, mas ainda
aparece bastante em código de bibliotecas reutilizáveis.
5. Executando tarefas em paralelo
Task<Cliente> tarefaCliente = ObterClienteAsync(1);
Task<List<Pedido>> tarefaPedidos = ObterPedidosAsync(1);
await Task.WhenAll(tarefaCliente, tarefaPedidos);
Cliente cliente = tarefaCliente.Result; // já concluída, seguro ler .Result aqui
List<Pedido> pedidos = tarefaPedidos.Result;
Chamar .Result ou .Wait() numa
Task ainda não concluída bloqueia a thread —
exatamente o problema que async/await existe para evitar — e em
certos contextos (UI, ASP.NET clássico) pode travar a aplicação inteira num deadlock. Sempre
prefira await de ponta a ponta.
6. Async "até o fim" — por que não dá para misturar
Uma vez que um método é async, essa natureza tende a se propagar para
quem o chama — daí a expressão "async all the way down". Tentar "voltar a ser síncrono" no meio do
caminho (chamando .Result) reintroduz justamente o bloqueio que se
queria evitar.
7. Onde isso te leva
Você já usa async/await desde o Módulo 2 (todos os exemplos de integração com C# eram assíncronos). O próximo capítulo cobre delegates, eventos e expressões lambda — as peças que tornam possível passar "comportamento" como se fosse dado, algo que você já faz naturalmente em JavaScript com funções de primeira classe.