← Back to Articles         
C# Asynchronous Programming – Advanced Concepts

C# Asynchronous Programming – Advanced Concepts

Published on 12 Oct 2026     5 min read C#
Asynchronous Programming (Async/Await)

1. What is the difference between Task and ValueTask?

Both Task and ValueTask represent asynchronous operations in C#.

The main difference is memory allocation and performance.

Task

ValueTask

Reference type (class)

Value type (struct)

Typically involves a Task object

Can avoid Task allocation

Easy to use

Requires careful handling

Can be awaited multiple times

Generally should be awaited only once

Recommended for most async methods

Useful for frequently synchronous results

Example

// Using Task
public async Task<int> GetNumberAsync()
{
    await Task.Delay(1000);
    return 100;
}

// Using ValueTask
public ValueTask<int> GetNumberAsync()
{
    return ValueTask.FromResult(100);
}

Explanation:

  • Task<int> represents an asynchronous operation returning an integer.

  • ValueTask<int> can return an integer immediately without allocating a new Task object.

  • ValueTask is useful when the result is frequently available synchronously, such as from a cache.

Key Points

  • Prefer Task for normal asynchronous programming.

  • Use ValueTask when performance measurements justify it.

  • ValueTask is not automatically faster than Task.

  • Avoid awaiting the same ValueTask multiple times unless its source explicitly supports it.

 

2. What are asynchronous streams using IAsyncEnumerable<T>?

IAsyncEnumerable<T> allows us to retrieve and process data asynchronously, one item at a time, instead of waiting for the entire collection.

It is useful for large datasets, paginated APIs, and streaming data.

Example

public async IAsyncEnumerable<int> GetNumbersAsync()
{
    for (int i = 1; i <= 5; i++)
    {
        await Task.Delay(1000);
        yield return i;
    }
}

// Calling method
await foreach (int number in GetNumbersAsync())
{
    Console.WriteLine(number);
}

Output:

1  (after 1 second)
2  (after 2 seconds)
3  (after 3 seconds)
4  (after 4 seconds)
5  (after 5 seconds)

Explanation:

  • IAsyncEnumerable<int> returns an asynchronous sequence of integers.

  • yield return produces one value at a time.

  • await foreach asynchronously retrieves each value.

  • The application does not need to wait for all five values before processing the first.

Flow Diagram

GetNumbersAsync()
       |
       v
Wait 1 second
       |
       v
Return 1 ---> Process 1
       |
Wait 1 second
       |
       v
Return 2 ---> Process 2
       |
      ...
       |
Return 5 ---> Process 5

Key Points

  • Introduced in C# 8.0.

  • Uses IAsyncEnumerable<T>, yield return, and await foreach.

  • Suitable for large datasets and streaming.

  • Avoids buffering the entire sequence when items can be processed individually.

  • Supports cancellation using CancellationToken.

 

3. How do exceptions propagate in async methods?

When an exception occurs inside an asynchronous method returning Task or Task<T>, the exception is stored in the returned Task.

When the caller uses await, the exception is rethrown and can be handled using try-catch.

Example

public async Task<int> DivideAsync()
{
    await Task.Delay(1000);

    int a = 10;
    int b = 0;

    return a / b;
}

// Calling method
try
{
    int result = await DivideAsync();
    Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("Error: " + ex.Message);
}

Output:

Error: Attempted to divide by zero.

Explanation:

  1. DivideAsync() starts execution.

  2. Task.Delay() introduces an asynchronous wait.

  3. Division by zero causes an exception.

  4. The returned Task becomes faulted.

  5. await rethrows the exception.

  6. The caller's catch block handles it.

Flow Diagram

Caller
   |
   v
await DivideAsync()
   |
   v
Exception occurs
   |
   v
Task becomes Faulted
   |
   v
await rethrows exception
   |
   v
catch block handles it

Key Points

  • Exceptions from Task-returning async methods are captured in the Task.

  • await rethrows the original exception type.

  • Use try-catch around await.

  • Task.WhenAll() can store multiple exceptions; a single await generally throws one of them.

  • async void exceptions cannot be handled by awaiting the method and may crash the application.

 

4. What happens when an async method is called without awaiting it?

When an asynchronous method is called without await, the method starts executing, but the caller does not wait for its completion.

This is sometimes called fire-and-forget, although simply discarding a Task does not provide safe background execution.

Example

public async Task SendEmailAsync()
{
    await Task.Delay(3000);
    Console.WriteLine("Email sent");
}

// Calling method
public async Task ProcessAsync()
{
    SendEmailAsync(); // No await

    Console.WriteLine("Process completed");

    await Task.Delay(4000);
}

Output:

Process completed
Email sent

Explanation:

  • SendEmailAsync() starts executing.

  • It reaches Task.Delay(3000) and returns an incomplete Task.

  • Because there is no await, the caller continues immediately.

  • "Process completed" prints first.

  • After approximately three seconds, "Email sent" prints.

The final Task.Delay(4000) keeps this example running long enough to observe the email operation. Without it, the operation might not finish before the application exits.

Correct Approach

public async Task ProcessAsync()
{
    await SendEmailAsync();

    Console.WriteLine("Process completed");
}

Output:

Email sent
Process completed

Key Points

  • An async method starts executing when called, even without await.

  • The caller continues without waiting for completion.

  • Exceptions may go unobserved.

  • The operation may not complete if the application shuts down.

  • In ASP.NET Core, avoid untracked fire-and-forget operations that depend on scoped services such as DbContext.

  • For reliable background processing, use BackgroundService or a background job queue.

 

Quick Interview Revision

Concept

Important Point

Task

Standard way to represent asynchronous operations

ValueTask

Can reduce allocations when results are frequently synchronous

IAsyncEnumerable<T>

Asynchronously processes items one by one

Async exceptions

Stored in Task and rethrown by await

Async without await

Caller continues without waiting; exceptions and completion need explicit handling

Interview tip: For ASP.NET Core Web API development, prefer Task and async/await for database calls, HTTP requests, and other I/O operations. Use ValueTask only when a measured performance benefit justifies its additional complexity.

← Back to Articles