When page script calls an exposed C++ function, the page pauses and waits for that function to return. If that function does slow work (such as reading a file, making a network request, or running a heavy calculation), the page and your UI freeze.

Making the function asynchronous avoids this— the function hands the page a Promise right away, does its work elsewhere, and settles the Promise when finished.

## Choosing Between Resolvers and Tasks

You can write an exposed asynchronous function using either a `js::Resolver` or a `js::Task`.

Page script cannot tell them apart— both return standard JavaScript Promises, and you can mix both styles freely across your application.

| | `js::Resolver` | `js::Task<T>` |
|---|---|---|
| **What it is** | A handle that settles one Promise | A C++ coroutine that settles on return |
| **How you write it** | Take a trailing `js::Resolver` parameter and call `Resolve()` or `Reject()` later | Return `js::Task<T>`, using `co_await` and `co_return` |
| **Where it settles** | Any thread | The Renderer's thread (with `js::RunOnWorker()` for background work) |
| **Waiting on page promises** | `Value::Then()` | `co_await js::Await()` |
| **Programming style** | Callback-based | Sequential async function |

### When to Use Each Style

- `js::Resolver`— use this when another native system reports the result (such as a worker queue, an OS event, or a background network callback). It fits single-step tasks where an outside system signals completion.
- `js::Task`— use this for sequential multi-step operations (such as loading an asset on a worker thread, processing it, and querying the page for input). It reads top to bottom like an asynchronous JavaScript function.

In both styles, if page script calls an exposed C++ function with invalid arguments, the returned Promise rejects with a `TypeError` and the C++ function never runs.

## Working with Resolvers

A `js::Resolver` lets native code settle a single Promise directly. You can move the resolver onto another thread or pass it to a background job, then call `Resolve()` or `Reject()` when the work completes.

Define an exposed C++ function that returns `void` and takes a `js::Resolver` as its final parameter:

```cpp
api["save"] = [](std::string path, js::Resolver done) {
  worker_queue.Push([path, done = std::move(done)]() mutable {
    if (WriteSave(path))
      done.Resolve(true);
    else
      done.Reject(js::Error::Make(js::ErrorType::Error, "disk full"));
  });
};
```

Page script awaits the returned Promise normally:

```js
const saved = await myApp.save("slot1.dat");
```

### Settling the Promise

Resolvers follow these settlement rules:

- You can call `Resolve()` or `Reject()` from any thread.
- The value passed to `Resolve()` is copied or moved immediately, then converted to JavaScript on the Renderer's thread during `Renderer::Update()`.
- Only the first call to `Resolve()` or `Reject()` takes effect— later calls are ignored.
- If a resolver is destroyed before settling, it rejects the Promise automatically so the page never hangs.

### Type Restrictions

- A `js::Resolver` is move-only and cannot be stored directly in a `std::function`. Wrap it in a `std::shared_ptr` if you need to pass it to a copyable callable.
- Call-only types cannot be resolved. For non-owning views like `std::span`, copy the data into an owning container before resolving. For raw instance pointers, pass an owning holder instead.

### Creating Promises Outside an Exposed Function

Native code can also give the page a pending Promise without an exposed function (eg, as a property the page reads). Call `Context::MakePromise()` to get a `js::PendingPromise` holding both the page's Promise and a `js::Resolver` to settle it:

```cpp
auto [promise, resolver] = ctx.MakePromise();
ctx["ready"] = promise;

// Later, from any thread:
resolver.Resolve(std::string("ok"));
```

## Working with Tasks

A `js::Task<T>` is a C++ coroutine that returns a JavaScript Promise to the page. It reads from top to bottom, pausing at each `co_await` without blocking the Renderer's thread. Tasks use standard C++20 coroutines— because the Ultralight C++ API already requires C++20, you don't need extra compiler configuration.

Return `js::Task<T>` from an exposed C++ function:

```cpp
api["loadSave"] = [](std::string slot) -> js::Task<SaveData> {
  SaveData data =
      co_await js::RunOnWorker([slot] { return ReadSaveFromDisk(slot); });
  if (!data.valid)
    co_return js::Error::Make(js::ErrorType::Error, "save is corrupt")
        .WithCode("BAD_SAVE");
  co_return data;
};
```

Page script awaits the returned Promise normally:

```js
const save = await myApp.loadSave("slot1");
```

### JavaScript Equivalents

`js::Task` maps standard JavaScript asynchronous syntax directly to C++ coroutine keywords:

| JavaScript | C++ |
|---|---|
| `async function f() { ... }` | `[](...) -> js::Task<T> { ... }` |
| `return value;` | `co_return value;` |
| `throw new Error("...");` | `co_return js::Error::Make(...);` |
| `await somePromise` | `co_await js::Await<T>(promise)` (returns `js::Result<T>`) |
| `await fn(...)` | `co_await js::Await<T>(fn(...))` (returns `js::Result<T>`) |
| *(run on another thread)* | `co_await js::RunOnWorker([...] { ... })` (returns plain `T`) |

### How a Task Runs

Coroutines run under these execution rules:

- The task body starts immediately when the page calls the exposed C++ function and runs up to the first `co_await`.
- The coroutine body always runs on the Renderer's thread, both before and after every `co_await`.
- The Promise settles during a later `Renderer::Update()` after the coroutine completes.
- Function parameters remain valid for the entire task rather than expiring at the first `co_await`— this includes `const` references, `js::CallInfo`, and views like `std::string_view` and `std::span`.

### Reporting Errors

Use `co_return js::Error::Make(...)` to reject the Promise with an error, as shown in the save-loading example above.

A `js::Task<void>` cannot `co_return` an error. To report failures from a void operation, return `js::Task<bool>` instead, or use a `js::Resolver`.

When compiled with C++ exceptions enabled, any unhandled exception escaping the coroutine body automatically rejects the page's Promise.

### Running Work on a Background Thread

Use `co_await js::RunOnWorker(fn)` to run a function on a background thread. When the function returns, the coroutine resumes on the Renderer's thread with the function's plain return value rather than a `js::Result`.

Ultralight runs these functions on a small worker pool shared across the entire library. You should avoid long-running operations that occupy a worker thread for an extended time.

> 🚧 Keep JavaScript on the Renderer's Thread
>
> Never access JavaScript values or contexts inside the worker function. Types like `js::Value` and `js::Context` work only on the Renderer's thread. Pass plain C++ data into the worker function and return plain C++ data from it.

### Awaiting Page Promises

Inside a task, `co_await js::Await<T>()` pauses native code until a page Promise settles, returning a `js::Result<T>`.

Pass a call to a page function directly to `js::Await<T>()` to invoke it and await its returned Promise:

```cpp
api["confirm"] = [](js::Value ask) -> js::Task<bool> {
  js::Result<bool> answer = co_await js::Await<bool>(ask("Overwrite?"));
  co_return answer ? *answer : false;
};
```

Page script passes a callback that returns a Promise:

```js
const ok = await myApp.confirm((question) => showDialog(question));
```

#### What the Result Holds

The returned `js::Result<T>` holds one of these outcomes:

- **The Promise resolves**— the value converted to `T`.
- **The Promise rejects**— a `js::Error` holding the rejection reason.
- **Conversion fails**— a `TypeError` if the resolved value cannot convert to `T`.
- **The page goes away**— a page-gone error if the page navigates or closes before the Promise settles.
- **The call fails**— an error immediately if the function throws, the function handle is empty, or the page is already gone.

#### Awaiting Non-Promise Values

Like the `await` operator in JavaScript, `js::Await()` also accepts values that are not Promises.

If you pass a primitive value like a number or a string, the task resumes immediately with that value.

Any other object passes through the page's `Promise.resolve()` first, which can run page script.

#### Navigation and Cancellation

If the page closes or navigates away while a task is waiting, the task still resumes— awaiting a Promise from that page yields a page-gone error, and operations on the page's values fail safely. When the task finishes, the library discards its final result.

You can't cancel a running task directly. A task stops only when native code destroys the last owning handle of the `js::API` that exposed the function— while another owning handle to that `js::API` exists, tasks keep running.

Once that last handle is destroyed, the waiting task doesn't resume after its next `co_await` of `js::RunOnWorker()` or `js::Await()`. The rest of its body never runs, and the page's Promise rejects with a `TypeError` whose code is `"ULJS_DETACHED"`.

### Waiting on Promises Without Coroutines

If you need to wait on a JavaScript Promise without using C++ coroutines, call `Value::Then()`. It registers a callback that runs on the Renderer's thread during a later `Renderer::Update()` when the Promise settles.

If the `js::Value` is not a Promise, `Value::Then()` returns `false` immediately.

Call `Then()` on a `js::Value` to receive the settlement result:

```cpp
api["whenReady"] = [](js::Value promise) {
  bool waiting = promise.Then([](js::Result<js::Value> result) {
    if (result)
      LoadLevel(*result);
    else if (!result.error().is_page_gone())
      Log(result.error().message());
  });
  if (!waiting)
    Log("whenReady expects a Promise");
};
```

## Dispatching Work from Another Thread

To run code on the Renderer's thread from a background thread without returning a Promise, call `Context::PostTask()`. It schedules a callback to run on the Renderer's thread during a later `Renderer::Update()`.

You can call `PostTask()` from any thread. If the page closes before the task runs, Ultralight skips the callback safely.

Schedule a callback on the Renderer's thread using `PostTask()`:

```cpp
ctx.PostTask([](js::Context& live) {
  live["progress"] = 0.5;
});
```

## When Completions Arrive

All asynchronous completions arrive on the Renderer's thread during `Renderer::Update()`. This includes resolved `js::Resolver` instances, resumed `js::Task` coroutines, worker results from `js::RunOnWorker()`, callbacks scheduled with `Context::PostTask()`, and awaited JavaScript Promises.

> 👍 Keep Calling Update
>
> You must continue calling `Renderer::Update()` in your frame loop, or asynchronous completions will never arrive. See [Updating and Rendering](/docs/2.0/updating-and-rendering) for guidance on running the loop.
