JavaScript Errors and Diagnostics
Handle JavaScript failures, throw errors from native code, and configure development diagnostics.
On this page
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:
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.
🚧 Check Before Dereferencing
Calling
value()or dereferencing with*on a result that holds an error is undefined behavior. Check the result first or usevalue_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:
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.
Reading Error Details
You can read error properties to get more context about the failure:
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:
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:
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 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
codeproperty 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). |
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.javascriptlevel, never an individual API's level— the setting defaults toAuto(on with developer mode, off without). An API created atStrictwon'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:
(void)ctx.Evaluate("throw new Error('nobody reads me')");
The native logger outputs the unread exception:
[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:
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 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:
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
WarnorStrictis what addsul.diagnosticsto 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:
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). |
level(root) |
Effective diagnostics level as "off", "warn", or "strict", or null. |
stats() |
Counts of live native handles for this page. |