Skip to content

C# Feature - async Main

async Main allows a console applicationโ€™s entry point to return Task or Task<int>, so startup code can await asynchronous operations directly instead of blocking on them.

This feature was introduced in C# 7.1 (2017).

Before C# 7.1, async startup logic usually meant forcing asynchronous code into synchronous flow with .Result, .Wait(), or .GetAwaiter().GetResult(). That approach is harder to read and can hide failure behavior.

With async Main, the entry point follows the same async style as the rest of the codebase.

static async Task Main()
static async Task Main(string[] args)
static async Task<int> Main()
static async Task<int> Main(string[] args)
  • Use it in console applications that call HTTP APIs, databases, files, queues, or any other async dependency during startup.
  • Use Task<int> when the process should return an exit code.
  • Prefer it over blocking on async work manually.
static int Main()
{
RunAsync().GetAwaiter().GetResult();
return 0;
}
static async Task Main()
{
await RunAsync();
}
using System.Net.Http;
static async Task<int> Main(string[] args)
{
using var client = new HttpClient();
try
{
string health = await client.GetStringAsync("https://example.com/health");
Console.WriteLine($"Service health: {health}");
return 0;
}
catch (HttpRequestException ex)
{
Console.Error.WriteLine($"Startup check failed: {ex.Message}");
return 1;
}
}
  • Do not mix async Main with .Result or .Wait() unless there is a very specific reason.
  • async void is not a valid entry point signature.
  • Startup code can still fail fast, so exception handling should be deliberate rather than implicit.
  • C# 7.1 introduced asynchronous entry points.
  • C# 9 top-level statements can also produce an async entry point implicitly when await is used.