Async Support

Transpose supports async and await, backed by the JavaScript event loop. C# async methods compile to native JavaScript async functions, and System.Threading.Tasks.Task is the runtime's own task type, adapted to and from native promises at the boundaries.

Async/await

Write asynchronous code exactly as you would in .NET:

public async Task<string> FetchDataAsync(string url)
{
    var response = await Http.GetAsync(url);
    return response.Text;
}

public static async Task Main()
{
    try
    {
        string data = await FetchDataAsync("https://api.example.com/data");
        Console.WriteLine(data);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Error: " + ex.Message);
    }
}

An async Task Main entry point is supported: the runtime awaits it and surfaces a faulted result rather than dropping it.

How it works

Transpose does not rewrite an async method into a state machine. It emits a native JavaScript async function and lets the engine drive it:

  • The body of a C# async method is emitted as an inner native async arrow function, invoked immediately. The promise it returns is wrapped back into a Task, so the method's return value is a Task on both the C# and the JavaScript side.
  • Each await in the body is emitted as a native await over an adapter that accepts either a Task or a native promise.

Task is not a native promise

This is the main thing to know when you cross into JavaScript. The runtime's Task is its own type — it exposes continueWith, isFaulted, getAwaitedResult — and it is not thenable. C# await works on it because the compiler routes every awaited expression through an adapter that converts a Task (or a getAwaiter()-shaped object, or an already-native promise) into a promise the engine can await.

Practically:

From To What you do
C# await over a C# Task Nothing; just await.
C# await over a JS promise Type the extern as Task/Task<T> and await it.
JS consuming a C# Task promise Wrap it: Transpose.toPromise(task).then(…).

Interoperating with JavaScript promises

Consuming a promise

Declare the external function as returning Task or Task<T> and await it. The adapter recognises a native promise and awaits it directly, with no conversion step of your own.

[External]
public static class MyJsLib
{
    public static extern Task<int> LongRunningOperation();
}

// Usage
int result = await MyJsLib.LongRunningOperation();

A rejected promise becomes a thrown exception, so an ordinary try/catch around the await works.

Returning a task to JavaScript

A C# method that returns Task returns a runtime Task in JavaScript, so a caller has to adapt it:

public static async Task<string> GetData()
{
    await Task.Delay(1000);
    return "Hello from C#";
}
// JavaScript — Task is not thenable, so adapt it first.
Transpose.toPromise(MyNamespace.MyClass.GetData()).then(result => {
    console.log(result); // "Hello from C#"
});

Transpose.toPromise also accepts a value or a promise unchanged, so it is safe to apply unconditionally.

If the boundary matters to you, an alternative is to hand JavaScript a plain callback (Action<T>) and invoke it from C# when the work completes — no adaptation needed on either side.

Task.Delay

Task.Delay is implemented with setTimeout.

await Task.Delay(1000); // resumes about a second later

It honours a CancellationToken, which is also implemented on timers.

Considerations

  • There is one thread. await yields to the event loop; it does not move work to a background thread. A long synchronous computation blocks rendering and input regardless of how many awaits surround it. Break such work into chunks separated by await Task.Delay(0), or move it to a Web Worker through a Transpose.Core binding.
  • Task.Run does not offload work. It schedules the delegate on the event loop and wraps the result in a Task. The delegate still runs on the main thread; the only thing you gain is that it runs later.
  • System.Threading.Thread has no browser equivalent and is not supported. Neither are the blocking waits (Task.Wait(), .Result, Task.WaitAll) — there is no thread to block, so await is the only option.
  • Fire-and-forget async void works, but an exception thrown in it has nowhere to surface. Prefer async Task and await it, or catch inside.
© 2026 Curiosity. All rights reserved.