StringBuilder
async method
C#
asynchronous programming
.NET

Adding string to StringBuilder from async method

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Using StringBuilder inside an async method is perfectly valid, but many developers mix up asynchronous execution with thread safety. await does not automatically make StringBuilder unsafe, and it does not automatically make it safe either. The real question is whether one logical flow owns the builder or several concurrent operations are trying to append to it at the same time.

The Safe Case: One Async Flow Owns the Builder

If the StringBuilder is used by one method that awaits work and then appends results, the pattern is usually fine:

csharp
1using System;
2using System.Net.Http;
3using System.Text;
4using System.Threading.Tasks;
5
6class Program
7{
8    static async Task Main()
9    {
10        var builder = new StringBuilder();
11        await AppendPageTitleAsync(builder);
12        Console.WriteLine(builder.ToString());
13    }
14
15    static async Task AppendPageTitleAsync(StringBuilder builder)
16    {
17        using var client = new HttpClient();
18        string html = await client.GetStringAsync("https://example.com");
19        builder.AppendLine($"Downloaded {html.Length} characters");
20    }
21}

There is no race here. The method pauses while waiting for I/O, then resumes and appends. The builder is still being mutated by one logical operation.

Where Problems Start

The danger appears when several tasks share the same StringBuilder concurrently. StringBuilder is not thread-safe for simultaneous writes.

csharp
1var builder = new StringBuilder();
2
3var task1 = Task.Run(() => builder.AppendLine("first"));
4var task2 = Task.Run(() => builder.AppendLine("second"));
5
6await Task.WhenAll(task1, task2);

That code may appear to work in simple tests, but it is relying on behavior that is not guaranteed. Concurrent appends can interleave unpredictably.

The fix is to avoid shared mutable state when tasks run in parallel. A better approach is to let each task produce a string and combine results afterward.

Prefer Returning Strings from Async Work

Instead of passing a shared builder into several asynchronous operations, let each operation return its own text and append in one place:

csharp
1using System.Linq;
2using System.Text;
3using System.Threading.Tasks;
4
5static async Task<string> BuildLineAsync(int value)
6{
7    await Task.Delay(50);
8    return $"Value: {value}";
9}
10
11static async Task<string> BuildReportAsync()
12{
13    var lines = await Task.WhenAll(
14        BuildLineAsync(1),
15        BuildLineAsync(2),
16        BuildLineAsync(3));
17
18    var builder = new StringBuilder();
19    foreach (var line in lines)
20    {
21        builder.AppendLine(line);
22    }
23
24    return builder.ToString();
25}

This keeps concurrency at the string-producing level and keeps mutation of the builder single-threaded.

When Locking Is Acceptable

If several tasks must append to one shared builder, you need synchronization:

csharp
1private static readonly object _gate = new object();
2
3static void SafeAppend(StringBuilder builder, string text)
4{
5    lock (_gate)
6    {
7        builder.AppendLine(text);
8    }
9}

This works, but it is often a sign that the design can be simplified. Shared mutable state makes asynchronous code harder to reason about and harder to test.

Performance Considerations

StringBuilder is useful when you are creating a larger string from many fragments. It is not automatically the best tool in every async method. If you only join a small set of completed strings once, string.Join may be clearer.

Also remember that StringBuilder optimizes allocations, not network latency. If the async method is slow, the bottleneck is probably I/O, not string concatenation.

Common Pitfalls

  • Assuming async means code runs on multiple threads all the time.
  • Sharing one StringBuilder across several concurrent tasks without synchronization.
  • Passing mutable state everywhere instead of returning strings and combining them later.
  • Adding locks reflexively when a simpler ownership model would avoid the problem entirely.
  • Optimizing string assembly when the real delay is database or HTTP latency.

Summary

  • Using StringBuilder inside an async method is fine when one logical flow owns it.
  • 'StringBuilder becomes unsafe when several concurrent tasks mutate it at the same time.'
  • A cleaner pattern is to let async methods return strings and append in one place.
  • Locking can make shared appends safe, but it usually increases complexity.
  • Focus on ownership and concurrency, not just on the presence of async and await.

Course illustration
Course illustration

All Rights Reserved.