You can handle JavaScript failures in native code, throw errors into the page, and catch API integration mistakes with diagnostics.

Calls into a page can fail if a script throws or the page navigates away. The JavaScript API reports these failures as values instead of throwing C++ exceptions.

Errors pass in both directions— the page sends errors to native code, and native code can throw errors back into the page.

## Handling Failed Calls

Operations that can fail return a `js::Result<T>` holding either the converted value or a `js::Error`:

```cpp
js::Result<double> score = ctx.Evaluate<double>("score");
if (score)
  Log("score: " + std::to_string(*score));
else
  Log(score.error().message());
```

`js::Result` works like `std::expected`. You can check `if (score)` to verify success, dereference it with `*` for the value, or call `error()` to inspect the failure.

You can also use `js::Or()` to convert a value with a fallback, as covered in [Calling into the Page](/docs/2.0/calling-into-the-page).

> 🚧 Check Before Dereferencing
>
> Calling `value()` or dereferencing with `*` on a result that holds an error is undefined behavior. Check the result first or use `value_or()` to provide a fallback, since the library never throws C++ exceptions.

## Identifying the Error

A `js::Error` represents one of four failure conditions:

| Kind | Check | Cause |
| --- | --- | --- |
| JavaScript exception | `is_exception()` | Script threw an error, or executed an invalid JavaScript operation (eg, calling something that is not a function). |
| Empty handle | `is_empty()` | The `js::Value` or `js::Context` holds nothing because it was never assigned, was moved from, or came from a failed read. This usually indicates a bug in native code. |
| Page gone | `is_page_gone()` | The page navigated away, its frame was removed, or its View or Renderer was destroyed. |
| Native error | All three are false | Created in C++ by native code or by the library (eg, passing the wrong type to `To<T>()`). |

A page can go away at any time, so page-gone errors usually need no reporting:

```cpp
js::Result<std::string> title = ctx.Evaluate<std::string>("document.title");
if (!title) {
  if (title.error().is_page_gone())
    return;                        // the page went away (nothing to report)
  Log(title.error().message());    // threw, empty handle, or not a string
}
```

For more details on handle states and lifecycles, see [Working with JavaScript Values](/docs/2.0/working-with-javascript-values).

### Reading Error Details

You can read error properties to get more context about the failure:

```cpp
const js::Error& error = result.error();
if (error.is_exception())
  Log(error.message() + " at line " + std::to_string(error.line()));
```

The `js::Error` provides several accessors depending on the kind of error:

| Method | Description |
| --- | --- |
| `message()` | Human-readable description of the error. |
| `code()` | The error code string, or an empty string if none was set. |
| `type()` | The `js::ErrorType` (`Error`, `TypeError`, `RangeError`, `SyntaxError`, or `ReferenceError`). |
| `line()`, `column()` | Line and column numbers in the script where the exception occurred (JavaScript exceptions only). |
| `source_url()` | URL of the script that threw the exception (JavaScript exceptions only). |
| `stack()` | Call stack string for the exception (JavaScript exceptions only). |

## Throwing Errors into the Page

An exposed native function throws an error into the page by returning a `js::Unexpected` wrapping a `js::Error`:

```cpp
api["loadLevel"] = [](std::string name) -> js::Result<double> {
  if (!LevelExists(name))
    return js::Unexpected(js::Error::RangeError("no such level: " + name)
                              .WithCode("MYAPP_NO_LEVEL"));
  return LoadLevel(name);
};
```

The page catches the error and can check its `code` property:

```js
try {
  myApp.loadLevel("castle");
} catch (e) {
  if (e.code === "MYAPP_NO_LEVEL")
    showLevelPicker();
}
```

You can construct errors using `js::Error::TypeError(message)`, `js::Error::RangeError(message)`, or `js::Error::Make(type, message)`. Calling `WithCode()` attaches a code string to the error so the page can identify the problem without parsing human-readable messages.

Async functions reject their Promise instead of throwing directly. If the function returns a `js::Task`, use `co_return` with the error. If it receives a `js::Resolver`, call `Reject(error)`. See [Async Callbacks](/docs/2.0/async-callbacks) for both approaches.

If C++ exceptions are enabled and an exposed function or task throws, the library catches it. The page receives a generic `Error` with no error code (or a rejected Promise for an async call). The exception's `what()` string goes to the native log as a warning at every diagnostics level— it never reaches the page. Return a `js::Unexpected` when native code needs to send a specific message or error code to the page.

## Error Codes

TypeErrors created by the library include a `code` property that you can read using `code()` in native code or `error.code` in page script.

> 👍 Check Error Codes Instead of Messages
>
> Check the `code` property rather than the message text when handling errors. Error messages are written for people and can change between releases, while error codes remain stable.

| Code | Meaning |
| --- | --- |
| `ULJS_BAD_ARG` | A value (such as an argument, return value, property, or Promise result) does not have the expected type, or its text does not parse. |
| `ULJS_LOSSY` | A number does not fit the parameter's type (`Strict` diagnostics only). |
| `ULJS_EXTRA_ARGS` | A call passed more arguments than the function accepts (`Strict` diagnostics only). |
| `ULJS_DETACHED` | The native object or `js::API` behind a function or instance is gone. |
| `ULJS_NOT_INSTANCE` | A class method was called on an object that is not an instance of the class. |
| `ULJS_NO_CTOR` | Page script used `new` on a class that has no constructor. |
| `ULJS_CROSS_CONTEXT` | A value was used on a page that cannot share it (see [Working with JavaScript Values](/docs/2.0/working-with-javascript-values)). |

## Logging Unread Exceptions

When JavaScript diagnostics are on, destroying a `js::Error` that holds a script exception (an `is_exception()` error) without reading it logs a warning under the `[js]` tag.

> 🚧 Controlled by Global Configuration
>
> Logging unread exceptions follows the global `Config::diagnostics.javascript` level, never an individual API's level— the setting defaults to `Auto` (on with developer mode, off without). An API created at `Strict` won't log unread exceptions if developer mode is off.

This applies only to exceptions returned to native code in a `js::Result`, not to uncaught errors within page script.

Calling any accessor (such as `message()`, `is_page_gone()`, or `code()`) counts as reading the error. This behavior ensures that dropping a `js::Result` or using a `js::Or()` fallback still reports script errors during development.

If native code discards a result from a script that throws:

```cpp
(void)ctx.Evaluate("throw new Error('nobody reads me')");
```

The native logger outputs the unread exception:

```text
[js] A JavaScript exception was never read (the call's result was dropped): Error: nobody reads me
```

## Diagnostics Levels

The JavaScript API generates development warnings using the `[js]` tag, sending them to both the native `Logger` and the page console.

For example, reading a misspelled property on an API namespace logs a warning with a suggested correction:

```js
console.log(myApp.verison);
// Console: 'myApp.verison' is not defined. Did you mean 'version'?
```

The API supports three diagnostics levels:

| Level | Reports |
| --- | --- |
| `Off` | Disables per-call warnings. Mistakes detected during registration (such as an invalid binding path) still warn, and argument type checks still throw a `TypeError`. |
| `Warn` | Reports misspelled properties on namespace objects with suggestions, undeclared events, numbers that lose precision, bindings withheld by origin rules or filters, and reserved member names (`then`, `toJSON`, `constructor`, `__proto__`). |
| `Strict` | Includes everything in `Warn`, and turns lossy numeric arguments (`ULJS_LOSSY`) and extra arguments (`ULJS_EXTRA_ARGS`) into `TypeError` exceptions. |

An API defaults to `Auto`, which uses the level in `Config::diagnostics.javascript`. That field also defaults to `Auto`— it becomes `Warn` when developer mode is on and `Off` when it's off (whether the build is debug or release plays no part). Native code can set this field to `Off`, `Warn`, or `Strict` to control every API left at `Auto`. An API given its own level keeps it.

See [Developer Mode and Diagnostics](/docs/2.0/developer-mode-and-diagnostics) for details on enabling developer mode and setting environment overrides.

### Setting an API Level

You can set an API's diagnostics level when creating it:

```cpp
js::API api("myApp", { .diagnostics = js::Diagnostics::Strict });
```

You can also change the level later with `set_diagnostics()`, or check the current level with `diagnostics()`.

> 🚧 Set Levels Before Attaching
>
> Set the diagnostics level before attaching the API to a View. Attaching at `Warn` or `Strict` is what adds `ul.diagnostics` to the page— raising the level later will not add it.

### Inspecting Diagnostics on the Page

When an API attaches to a View at `Warn` or `Strict`, the page receives the `ul.diagnostics` object to inspect attached APIs:

```js
console.log(ul.diagnostics.apis());  // ["myApp", "ul.diagnostics"]
console.log(ul.diagnostics.level("myApp")); // "warn"
```

The `ul.diagnostics` object is absent on pages without active diagnostics (such as shipped builds where developer mode is disabled). Page script must never depend on it.

| Function | Returns |
| --- | --- |
| `apis()` | Root names of the APIs available on this page. |
| `schema(root)` | The API's schema object, or `null` if not on this page (see [API Schemas and TypeScript](/docs/2.0/api-schemas-and-typescript)). |
| `level(root)` | Effective diagnostics level as `"off"`, `"warn"`, or `"strict"`, or `null`. |
| `stats()` | Counts of live native handles for this page. |
