Task results
asynchronous programming
task management
.NET
C#

How to pass Task results to other Tasks not using continuations

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In modern C#, you usually do not need explicit continuations to pass the result of one Task into another. await already gives you a clean way to wait for a result and feed it into the next async step, and for more advanced pipelines you can use channels or TaskCompletionSource.

The Simplest Pattern: await Then Call the Next Method

If task B depends on the result of task A, write that dependency directly in code. This is clearer than ContinueWith and respects normal exception flow.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static async Task<int> GetOrderIdAsync()
7    {
8        await Task.Delay(100);
9        return 42;
10    }
11
12    static async Task<string> LoadOrderAsync(int orderId)
13    {
14        await Task.Delay(100);
15        return $"Order #{orderId}";
16    }
17
18    static async Task Main()
19    {
20        int orderId = await GetOrderIdAsync();
21        string order = await LoadOrderAsync(orderId);
22        Console.WriteLine(order);
23    }
24}

This is the direct replacement for most continuation-based code. The result is explicit, exceptions are propagated naturally, and the control flow is easy to debug.

Encapsulate the Whole Pipeline in One Async Method

If several tasks form one logical operation, hide the chaining inside a single async method rather than making callers stitch tasks together manually.

csharp
1using System.Threading.Tasks;
2
3public class InvoiceService
4{
5    public async Task<string> BuildInvoiceSummaryAsync()
6    {
7        int customerId = await GetCustomerIdAsync();
8        decimal balance = await GetBalanceAsync(customerId);
9        return $"Customer {customerId} owes {balance:C}";
10    }
11
12    private Task<int> GetCustomerIdAsync() => Task.FromResult(7);
13
14    private Task<decimal> GetBalanceAsync(int customerId) =>
15        Task.FromResult(125.50m);
16}

This pattern keeps dependent async work in one place and avoids exposing intermediate plumbing to the rest of the application.

Passing Results Between Independent Tasks

If one task produces values over time and another task consumes them, a channel is usually a better fit than continuations. A Channel<T> gives you safe producer-consumer flow with backpressure support.

csharp
1using System;
2using System.Threading.Channels;
3using System.Threading.Tasks;
4
5class Program
6{
7    static async Task Main()
8    {
9        var channel = Channel.CreateUnbounded<int>();
10
11        Task producer = Task.Run(async () =>
12        {
13            for (int i = 1; i <= 3; i++)
14            {
15                await channel.Writer.WriteAsync(i * 10);
16            }
17            channel.Writer.Complete();
18        });
19
20        Task consumer = Task.Run(async () =>
21        {
22            await foreach (int value in channel.Reader.ReadAllAsync())
23            {
24                Console.WriteLine($"received {value}");
25            }
26        });
27
28        await Task.WhenAll(producer, consumer);
29    }
30}

This is a good answer when "pass results to another task" means ongoing communication rather than a single handoff.

When TaskCompletionSource Makes Sense

TaskCompletionSource<T> is useful when another part of the system decides when a task completes. It is not the first tool to reach for, but it can bridge event-based or callback-based code into the task world.

csharp
1using System.Threading.Tasks;
2
3public class ResultHub
4{
5    private readonly TaskCompletionSource<string> _tcs = new();
6
7    public Task<string> WaitForResultAsync() => _tcs.Task;
8
9    public void Publish(string value) => _tcs.TrySetResult(value);
10}

One task can await WaitForResultAsync, while another part of the code publishes the value later. This avoids ContinueWith, but it should be used intentionally because manual task completion adds complexity.

Choosing the Right Tool

Use await when the dependency is simple and sequential. Use a channel when tasks exchange a stream of results. Use TaskCompletionSource<T> when you need an externally controlled completion signal. Most codebases are better when continuation chains disappear and business logic becomes normal async methods with explicit inputs and outputs.

Common Pitfalls

  • Replacing ContinueWith with Task.Result can deadlock or block threads unnecessarily.
  • Starting background tasks just to pass one value often makes the code harder to reason about.
  • Using TaskCompletionSource<T> for ordinary sequencing adds manual state management you do not need.
  • Forgetting to propagate cancellation tokens makes task pipelines harder to stop cleanly.
  • Swallowing exceptions inside fire-and-forget tasks hides failures from the caller that needed the result.

Summary

  • Use await for the normal case where one task depends on another task's result.
  • Wrap multi-step async flows in a single async method to keep the API clean.
  • Use Channel<T> for producer-consumer patterns that exchange many results.
  • Use TaskCompletionSource<T> only when completion must be controlled externally.
  • Prefer explicit async composition over continuation chains for readability and error handling.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.