Tsonic GitHub
Edit on GitHub

Async Patterns

Tsonic compiles TypeScript's async/await to C#'s Task-based async pattern. This guide covers async functions, for-await loops, and async generators.

Overview

Async functions return Promise<T> in TypeScript, which compiles to Task<T> in C#:

export async function fetchData(): Promise<string> {
  return "data";
}

Generated C#:

public static async Task<string> fetchData()
{
    return "data";
}

Async Functions

Basic Async Function

import { Console } from "@tsonic/dotnet/System.js";
import { Task } from "@tsonic/dotnet/System.Threading.Tasks.js";

async function delay(ms: number): Promise<void> {
  await Task.delay(ms);
}

export async function main(): Promise<void> {
  Console.writeLine("Starting...");
  await delay(1000);
  Console.writeLine("Done!");
}

Returning Values

import { Console } from "@tsonic/dotnet/System.js";
import { Task } from "@tsonic/dotnet/System.Threading.Tasks.js";

type User = {
  id: number;
  name: string;
};

async function fetchUser(id: number): Promise<User> {
  await Task.delay(10);
  return { id, name: "Alice" };
}

export async function main(): Promise<void> {
  const user = await fetchUser(123);
  Console.writeLine(user.name);
}

Error Handling

Use try/catch with async/await:

import { Console } from "@tsonic/dotnet/System.js";
import { Exception } from "@tsonic/dotnet/System.js";

async function riskyOperation(): Promise<string> {
  throw new Exception("Something failed");
}

export async function main(): Promise<void> {
  try {
    const result = await riskyOperation();
    Console.writeLine(result);
  } catch (e) {
    Console.writeLine("Error occurred");
  }
}

For-Await Loops

Use for await...of to iterate over async iterables.

Basic For-Await

import { Console } from "@tsonic/dotnet/System.js";
import { Task } from "@tsonic/dotnet/System.Threading.Tasks.js";

async function* asyncNumbers(): AsyncGenerator<number> {
  for (let i = 0; i < 5; i++) {
    await Task.delay(100);
    yield i;
  }
}

export async function main(): Promise<void> {
  for await (const n of asyncNumbers()) {
    Console.writeLine(`Got: ${n}`);
  }
}

Generated C#:

await foreach (var n in asyncNumbers())
{
    Console.WriteLine($"Got: {n}");
}

With IAsyncEnumerable

.NET's IAsyncEnumerable<T> works with for-await:

import { Console } from "@tsonic/dotnet/System.js";

declare function getItemsAsync(): AsyncIterable<string>;

export async function main(): Promise<void> {
  const items = getItemsAsync();
  for await (const item of items) {
    Console.writeLine(item);
  }
}

Collecting Async Results

import { Console } from "@tsonic/dotnet/System.js";
import { Task } from "@tsonic/dotnet/System.Threading.Tasks.js";

type Page = { index: number };

async function fetchPage(index: number): Promise<Page> {
  await Task.delay(10);
  return { index };
}

async function* fetchPages(): AsyncGenerator<Page> {
  for (let i = 1; i <= 10; i++) {
    yield await fetchPage(i);
  }
}

export async function main(): Promise<void> {
  const pages: Page[] = [];
  for await (const page of fetchPages()) {
    pages.push(page);
  }
  Console.writeLine(`Fetched ${pages.length} pages`);
}

Async Generators

Basic Async Generator

import { Console } from "@tsonic/dotnet/System.js";
import { Task } from "@tsonic/dotnet/System.Threading.Tasks.js";

async function* countdown(n: number): AsyncGenerator<number> {
  while (n > 0) {
    await Task.delay(1000);
    yield n;
    n--;
  }
}

export async function main(): Promise<void> {
  for await (const n of countdown(5)) {
    Console.writeLine(`${n}...`);
  }
  Console.writeLine("Liftoff!");
}

Bidirectional Async Generator

Async generators support bidirectional communication:

import { Console } from "@tsonic/dotnet/System.js";

async function* asyncAccumulator(
  start: number
): AsyncGenerator<number, number, number> {
  let total = start;
  while (true) {
    const received = yield total;
    total = total + received;
  }
}

export async function main(): Promise<void> {
  const gen = asyncAccumulator(10);

  const r1 = await gen.next();
  Console.writeLine(`Initial: ${r1.value}`); // 10

  const r2 = await gen.next(5);
  Console.writeLine(`After +5: ${r2.value}`); // 15

  const r3 = await gen.next(20);
  Console.writeLine(`After +20: ${r3.value}`); // 35
}

Async Yield Delegation

Delegate to other async generators with yield*:

async function* inner(): AsyncGenerator<string> {
  yield "a";
  await delay(100);
  yield "b";
}

async function* outer(): AsyncGenerator<string> {
  yield "start";
  yield* inner(); // Delegates asynchronously
  yield "end";
}

export async function main(): Promise<void> {
  for await (const s of outer()) {
    Console.writeLine(s); // start, a, b, end
  }
}

Parallel Execution

Promise.all Equivalent

Use array operations for parallel async:

async function fetchAllUsers(ids: number[]): Promise<User[]> {
  const results: User[] = [];
  for (const id of ids) {
    const user = await fetchUser(id);
    results.push(user);
  }
  return results;
}

For true parallelism, use .NET's Task APIs:

import { Task } from "@tsonic/dotnet/System.Threading.Tasks.js";

// Use Task.WhenAll for parallel execution

Common Patterns

Retry Pattern

async function withRetry<T>(
  operation: () => Promise<T>,
  retries: number
): Promise<T> {
  for (let i = 0; i < retries; i++) {
    try {
      return await operation();
    } catch (e) {
      if (i === retries - 1) throw e;
      await delay(1000 * (i + 1)); // Exponential backoff
    }
  }
  throw new Error("Should not reach here");
}

Timeout Pattern

import { CancellationTokenSource } from "@tsonic/dotnet/System.Threading.js";

async function withTimeout<T>(
  operation: () => Promise<T>,
  ms: number
): Promise<T> {
  const cts = new CancellationTokenSource();
  cts.cancelAfter(ms);
  // Use cancellation token with operation
  return await operation();
}

Producer-Consumer

async function* producer(): AsyncGenerator<number> {
  for (let i = 0; i < 10; i++) {
    await delay(100);
    yield i;
  }
}

async function consumer(): Promise<number> {
  let sum = 0;
  for await (const n of producer()) {
    sum = sum + n;
  }
  return sum;
}

Type Mappings

TypeScript C#
Promise<T> Task<T>
Promise<void> Task
AsyncGenerator<T> IAsyncEnumerable<T> (simplified)
AsyncGenerator<Y, R, N> Wrapper class with async methods

Async Entry Points

Main Function

export async function main(): Promise<void> {
  await someAsyncWork();
}

Generates:

public static async Task Main()
{
    await someAsyncWork();
}

Sync to Async Boundary

When calling async from sync code, use .Result or .Wait():

// In TypeScript, this would be at the entry point
import { Console } from "@tsonic/dotnet/System.js";

declare function syncWrapper(): string;

export function main(): void {
  const result = syncWrapper();
  Console.writeLine(result);
}

// This pattern is handled by the async main support

Limitations

No Promise Chaining

Tsonic does not support .then(), .catch(), or .finally():

// NOT SUPPORTED
declare const promise: Promise<number>;

promise.then((result) => result + 1);
promise.catch(() => 0);

// USE INSTEAD
export async function usePromise(): Promise<number> {
  try {
    const result = await promise;
    return result + 1;
  } catch {
    return 0;
  }
}

This is a deliberate design choice to ensure clean async/await code.

Avoid Promise Constructors

// Avoid Promise executor-style construction when targeting .NET tasks.
const promise = new Promise<number>((resolve) => {
  resolve(123);
});

// Prefer async functions that return Promise<T>.
export async function myOperation(): Promise<number> {
  return 123;
}

Best Practices

Always Await Promises

// Good
await asyncOperation();

// Bad - promise not awaited
asyncOperation(); // Fire-and-forget, may cause issues

Use Async Main

// Preferred
export async function main(): Promise<void> {
  await setup();
  await run();
  await cleanup();
}

Handle Errors at Boundaries

import { Console } from "@tsonic/dotnet/System.js";

export async function main(): Promise<void> {
  try {
    await application();
  } catch (e) {
    Console.writeLine("Fatal error");
    // Log and exit
  }
}

Avoid Mixing Patterns

declare function fetchA(): Promise<number>;
declare function fetchB(): Promise<number>;
declare function fetchC(): Promise<number>;

// Good - consistent async/await
export async function good(): Promise<void> {
  const a = await fetchA();
  const b = await fetchB();
  const c = await fetchC();

  void a;
  void b;
  void c;
}

// Avoid - mixing patterns
export async function bad(): Promise<void> {
  const a = await fetchA();
  void a;

  // Don't mix (also: Promise.then/catch/finally is not supported in Tsonic)
  fetchB().then(() => {
    // ...
  });
}

See Also