asynchronous programming
DownloadStringAsync
C# networking
task completion
async await

DownloadStringAsync wait for request completion

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

DownloadStringAsync belongs to the older WebClient API and does not return a Task, so you do not "wait" for it the same way you wait for modern async methods. Instead, it completes through an event callback. In current .NET code, the better answers are usually DownloadStringTaskAsync or, better still, HttpClient with await.

What DownloadStringAsync Actually Does

DownloadStringAsync starts an asynchronous download and returns immediately. Completion is reported through the DownloadStringCompleted event.

csharp
1using System;
2using System.Net;
3
4var client = new WebClient();
5client.DownloadStringCompleted += (sender, args) =>
6{
7    if (args.Error != null)
8    {
9        Console.WriteLine(args.Error.Message);
10        return;
11    }
12
13    Console.WriteLine(args.Result);
14};
15
16client.DownloadStringAsync(new Uri("https://example.com"));

That is the original event-based async pattern. There is no return value you can await directly.

If You Need to Wait, Prefer DownloadStringTaskAsync

WebClient also exposes a task-based wrapper that fits modern async code much better.

csharp
1using System;
2using System.Net;
3using System.Threading.Tasks;
4
5public static async Task Main()
6{
7    using var client = new WebClient();
8    string text = await client.DownloadStringTaskAsync("https://example.com");
9    Console.WriteLine(text);
10}

This is usually the cleanest answer if you are forced to stay on WebClient.

If You Must Block, Block on the Task Version, Not the Event Version

Sometimes legacy console or background code really needs a synchronous wait. If that is unavoidable, block on DownloadStringTaskAsync, not on DownloadStringAsync itself.

csharp
1using System;
2using System.Net;
3
4using var client = new WebClient();
5string text = client.DownloadStringTaskAsync("https://example.com").GetAwaiter().GetResult();
6Console.WriteLine(text);

This still blocks the current thread, so it is not ideal for UI code, but it is at least straightforward.

Avoid Blocking on UI Threads

If you call .Result, .Wait(), or .GetAwaiter().GetResult() on a UI thread, you risk deadlocks or frozen interfaces depending on the environment and synchronization context.

That is why desktop and mobile UI code should prefer await all the way.

Good pattern:

csharp
1private async Task LoadTextAsync()
2{
3    using var client = new WebClient();
4    string text = await client.DownloadStringTaskAsync("https://example.com");
5    Console.WriteLine(text);
6}

HttpClient Is the Modern Alternative

WebClient is legacy API. In modern .NET code, HttpClient is the preferred choice.

csharp
1using System;
2using System.Net.Http;
3using System.Threading.Tasks;
4
5public static async Task Main()
6{
7    using var http = new HttpClient();
8    string text = await http.GetStringAsync("https://example.com");
9    Console.WriteLine(text);
10}

This is the approach most new code should use unless you are maintaining an older codebase that already depends on WebClient.

Converting an Event to a Task Manually

If you absolutely have to work with the event API and cannot switch, you can bridge it into a TaskCompletionSource.

csharp
1using System;
2using System.Net;
3using System.Threading.Tasks;
4
5public static Task<string> DownloadAsTask(string url)
6{
7    var tcs = new TaskCompletionSource<string>();
8    var client = new WebClient();
9
10    client.DownloadStringCompleted += (sender, args) =>
11    {
12        client.Dispose();
13
14        if (args.Error != null)
15        {
16            tcs.SetException(args.Error);
17        }
18        else if (args.Cancelled)
19        {
20            tcs.SetCanceled();
21        }
22        else
23        {
24            tcs.SetResult(args.Result);
25        }
26    };
27
28    client.DownloadStringAsync(new Uri(url));
29    return tcs.Task;
30}

This is useful mainly when wrapping legacy APIs inside a modern async interface.

Common Pitfalls

  • Expecting DownloadStringAsync itself to be awaitable when it is event-based.
  • Blocking on async work from a UI thread and freezing the application.
  • Keeping new code on WebClient when HttpClient is a better fit.
  • Mixing event-based async and task-based async without a clear bridge.
  • Forgetting to handle errors and cancellation in the completion callback.

Summary

  • 'DownloadStringAsync uses the old event-based async pattern, so it does not return a Task.'
  • If you want to wait cleanly, use DownloadStringTaskAsync instead.
  • In modern .NET, prefer HttpClient with await.
  • Blocking is possible, but it should be avoided on UI threads.
  • If needed, wrap the event-based API in a TaskCompletionSource for cleaner async composition.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

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

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.