← Back to Articles         
Difference Between Task, Task<T>, and void

Difference Between Task, Task<T>, and void

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

C# Asynchronous Programming: Difference Between Task, Task<T>, and void

1. What are Task, Task<T>, and void?

In C#, Task, Task<T>, and void are return types used with methods, including asynchronous methods.

The main difference is whether the method returns a result and whether the caller can track its completion.

  • Task – Represents an asynchronous operation that does not return a value.

  • Task<T> – Represents an asynchronous operation that returns a value of type T.

  • void – Does not return a value or provide a task for the caller to await.

2. Comparison Table

Feature

Task

Task<T>

void

Returns a value

No

Yes

No

Can use await on the call

Yes

Yes

No

Tracks completion

Yes

Yes

No task returned

Exception handling

Through Task

Through Task

Special handling for async void

Recommended for async methods

Yes

Yes

Only event handlers

Example

Task SaveAsync()

Task<int> GetAsync()

void Click()

Important: A normal synchronous void method is perfectly valid. The restriction applies primarily to async void methods.

 

3. How do they work?

                  Async Method
                       |
          +------------+------------+
          |            |            |
          v            v            v
         Task       Task<T>        void
          |            |            |
          v            v            v
     No Result     Has Result   No Task
          |            |            |
          v            v            v
     Awaitable      Awaitable    Cannot Await
          |            |            |
          v            v            v
    Save Data      Get Data     Event Handler

A. Task – Asynchronous method without a return value

Use Task when an asynchronous operation performs some work but does not need to return a result.

Example program

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("Start");

        await SaveDataAsync();

        Console.WriteLine("End");
    }

    static async Task SaveDataAsync()
    {
        Console.WriteLine("Saving data...");

        await Task.Delay(2000);

        Console.WriteLine("Data saved successfully");
    }
}

Output

Start
Saving data...
(Wait 2 seconds)
Data saved successfully
End

Explanation

  1. Main() calls SaveDataAsync().

  2. SaveDataAsync() starts executing.

  3. Task.Delay(2000) represents an asynchronous wait.

  4. await suspends the method until the delay completes without blocking the thread during the wait.

  5. Once the operation completes, execution continues.

  6. The caller waits for the returned Task before printing End.

Execution flow

Main()
   |
   v
SaveDataAsync()
   |
   v
Start Saving
   |
   v
await Task.Delay(2000)
   |
   |  Asynchronous wait
   |
   v
Saving Completed
   |
   v
Task Completed
   |
   v
Main() Continues

Key Points

  • Task represents an asynchronous operation without a result value.

  • The caller can use await to wait for completion.

  • Exceptions are stored in the task and can be observed when awaiting it.

  • Commonly used for database inserts, updates, deletes, and file operations.

 

B. Task<T> – Asynchronous method with a return value

Use Task<T> when an asynchronous operation needs to return a result.

Here, T represents the return data type, such as int, string, bool, or a custom object.

Example program

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("Fetching employee...");

        int employeeId = await GetEmployeeIdAsync();

        Console.WriteLine($"Employee ID: {employeeId}");
    }

    static async Task<int> GetEmployeeIdAsync()
    {
        await Task.Delay(2000);

        return 101;
    }
}

Output

Fetching employee...
(Wait 2 seconds)
Employee ID: 101

Explanation

  1. Main() calls GetEmployeeIdAsync().

  2. The method returns a Task<int> representing the operation.

  3. Task.Delay(2000) simulates asynchronous work.

  4. After the delay, the method returns 101.

  5. await unwraps the completed Task<int> and provides its integer result.

  6. The result is assigned to employeeId.

Execution flow

Main()
   |
   v
GetEmployeeIdAsync()
   |
   v
await Task.Delay(2000)
   |
   v
return 101
   |
   v
Task<int> Completed
   |
   v
await extracts result
   |
   v
employeeId = 101

Key Points

  • Task<T> returns a result asynchronously.

  • T can be any valid return type.

  • The caller can use await to obtain the result.

  • Commonly used for fetching database records, calling APIs, and performing calculations.

For example, in ASP.NET Core:

public async Task<Employee?> GetEmployeeAsync(int id)
{
    return await _context.Employees.FindAsync(id);
}

The method returns an Employee object when found, or null otherwise.

 

C. void – Method without a return value

A normal void method does not return a result.

However, when we use async void, the caller cannot await its completion.

Example program

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("Start");

        DisplayMessage();

        Console.WriteLine("End");

        await Task.Delay(3000);
    }

    static async void DisplayMessage()
    {
        Console.WriteLine("Processing...");

        await Task.Delay(2000);

        Console.WriteLine("Processing completed");
    }
}

Output

Start
Processing...
End
Processing completed

Explanation

  1. Main() calls DisplayMessage().

  2. DisplayMessage() executes until reaching the incomplete await.

  3. Control returns to Main() because the method returns void.

  4. Main() prints End.

  5. After the delay, DisplayMessage() resumes and prints Processing completed.

The final Task.Delay(3000) keeps this demonstration running long enough for the message to appear. It does not await DisplayMessage() itself.

Execution flow

Main()
   |
   v
DisplayMessage()
   |
   v
Print "Processing..."
   |
   v
await Task.Delay(2000)
   |
   +------------------------+
   |                        |
   v                        v
Main() Continues       Async Operation
   |                        |
   v                        v
Print "End"             Delay Completes
                            |
                            v
                     Print "Completed"

Why should we avoid async void?

The main problems are:

  • The caller cannot use await to track completion.

  • Exceptions cannot be caught by awaiting the method.

  • Exception propagation depends on the synchronization context and can terminate the process.

  • Unit testing becomes difficult.

  • The caller cannot easily coordinate or cancel the operation.

Recommended usage

async void should generally be used only for event handlers requiring a void signature.

private async void Button_Click(object sender, EventArgs e)
{
    await SaveDataAsync();
}

In this case, handle exceptions inside the event handler where appropriate.

 

4. Exception Handling Differences

Consider a method that throws an exception.

Using Task

static async Task ProcessAsync()
{
    await Task.Delay(1000);

    throw new Exception("Processing failed");
}

The caller can catch the exception:

try
{
    await ProcessAsync();
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

Output

Processing failed

The exception is propagated when the task is awaited.

Using Task<T>

static async Task<int> GetValueAsync()
{
    await Task.Delay(1000);

    throw new Exception("Unable to get value");
}

Caller:

try
{
    int value = await GetValueAsync();
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

Output

Unable to get value

Using async void

static async void ProcessAsync()
{
    await Task.Delay(1000);

    throw new Exception("Processing failed");
}

A surrounding try-catch at the call site generally cannot catch this exception:

try
{
    ProcessAsync();
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

This is because the exception occurs after the method has returned control to the caller. It is not propagated through an awaitable task.

 

5. Real-World Example: ASP.NET Core Web API

In enterprise applications, we frequently use Task and Task<T> for database operations.

Service methods

public class EmployeeService
{
    private readonly AppDbContext _context;

    public EmployeeService(AppDbContext context)
    {
        _context = context;
    }

    // Task<T> - Returns employee details
    public async Task<Employee?> GetEmployeeAsync(int id)
    {
        return await _context.Employees.FindAsync(id);
    }

    // Task - Does not return a result
    public async Task SaveEmployeeAsync(Employee employee)
    {
        await _context.Employees.AddAsync(employee);

        await _context.SaveChangesAsync();
    }
}

Explanation

GetEmployeeAsync() uses Task<Employee?> because the caller expects an employee object.

SaveEmployeeAsync() uses Task because the method performs an operation without returning a value.

Note that EF Core's SaveChangesAsync() itself returns Task<int> (the number of state entries written), but the service deliberately does not expose that value.

Architecture flow

ASP.NET Core Controller
          |
          v
     EmployeeService
          |
          +-------------------------+
          |                         |
          v                         v
  GetEmployeeAsync()         SaveEmployeeAsync()
          |                         |
          v                         v
   Task<Employee?>                Task
          |                         |
          v                         v
   Returns Employee           Saves Employee
          |                         |
          +------------+------------+
                       |
                       v
                EF Core / SQL
 

6. Important Interview Concept: Task Does Not Always Mean a New Thread

A common misconception is that calling an asynchronous method automatically creates a background thread.

This is incorrect.

For example:

await Task.Delay(2000);

This does not keep a thread occupied for two seconds.

Instead, it registers a timer-based asynchronous operation and allows the current thread to perform other work.

Similarly, asynchronous database and HTTP calls can release the calling thread while waiting for I/O.

However, Task.Run() is different:

await Task.Run(() => PerformCalculation());

This typically schedules CPU-bound work on a ThreadPool thread.

Task.Delay / Async I/O
       |
       v
Non-blocking wait
       |
       v
No thread dedicated to waiting


Task.Run
       |
       v
ThreadPool Worker
       |
       v
Executes CPU-bound work
 

7. Key Interview Points

Question

Answer

Can Task return a value?

No

Can Task<T> return a value?

Yes

Can we await Task?

Yes

Can we await Task<T>?

Yes

Can we await async void?

No

Can Task throw exceptions?

Yes, exceptions are observable through the task

Is async void recommended?

Only for event handlers requiring it

Does Task always create a thread?

No

Can an async method return void?

Yes, but generally avoid it

Does await block the thread?

No, when awaiting an incomplete task

8. Interview Summary

Task is used when an asynchronous method performs an operation without returning a result.

Task<T> is used when an asynchronous method performs an operation and returns a result.

async void is generally reserved for event handlers because the caller cannot await its completion or observe its exceptions through a task.

Best practice: In ASP.NET Core and enterprise C# applications, prefer Task and Task<T> for asynchronous methods.

← Back to Articles