Working with JavaScript Values
Inspect, modify, and convert JavaScript values in native C++ code.
On this page
The js::Value class is used to represent live values in JavaScript. It supports a number of operations and provides convenient conversions to and from native types.
Example Code
The example code on this page uses the following monster object defined in page.html:
<html>
<head>
<script type="text/javascript">
// Our JavaScript object "monster"
var monster = {
name: "Vampire",
damage: 12,
level: "7",
loot: ["gold", "fang"]
};
</script>
</head>
<body>
<div id="hud"></div>
</body>
</html>
Lifetimes
Safe Handles with Graceful Degradation
A js::Value is a safe, RAII handle that points to a live JavaScript value— you can store it in your own objects and use it later without crashes when the page goes away. If an operation fails, the library returns an error and fails gracefully instead of throwing a C++ exception.
Garbage Collection
While handles are held, the JS garbage collector will not collect the underlying JavaScript value. Try not to hold onto unneeded handles to keep memory growth of the heap low.
Not Thread-Safe
Most operations must be performed on the Renderer thread (the thread you called Renderer::Create() or App::Create() on). (Copying, moving, and destroying handles are safe from any thread.)
🚧 Handles and Contexts Don't Persist Between Page Navigations
A handle keeps its JavaScript value from being garbage-collected but it doesn't keep its parent page alive. When a page navigates away, existing handles and contexts are no longer valid.
Handle Validity
Handle States
Handles can be in one of three states:
| State | When a handle is in it | if (val) |
Calls on it |
|---|---|---|---|
| Valid | Points to a JS value on a live page | true |
Work normally |
| Empty | An operation failed, or the handle was never initialized or was moved from | false |
Do nothing |
| Gone | Its page navigated away, its frame was removed, or its View was destroyed | false |
Do nothing |
Operations are safe on all three states (the API never throws exceptions even during chained operations on invalid handles)— error-checking is optional.
Working With Handles
You can cache a js::Value handle across calls to interact with a page object over time:
class MyApp : public LoadListener {
public:
// Inherited from LoadListener::OnDOMReady:
void OnDOMReady(View* caller, uint64_t frame_id, bool is_main_frame,
const String& url) override {
if (!is_main_frame)
return;
// Get the JS context for a View
js::Context ctx(caller);
// Cache a js::Value handle "monster_" to our page's "monster" object.
//
// NOTE: This handle's page is gone after each page navigation so
// we must re-initialize it after each "DOMReady".
//
// NOTE: This will prevent the JavaScript value on the page from being
// garbage collected but it will NOT keep the page / View alive.
//
monster_ = ctx["monster"];
}
// Called at some point during game execution:
void UpdateHUD() {
// Get 'damage' from our live 'monster' object as "std::optional<double>"
//
// NOTE: This will only succeed if all of the following are true:
// - the monster_ js::Value handle is valid
// - the property "damage" exists
// - the property can be converted to a double
//
// NOTE: The JS and DOM APIs are designed with graceful degradation in mind:
// a failed read returns std::nullopt or an error, never throws.
//
if (auto dmg = monster_["damage"].Maybe<double>()) {
// Use the optional value as a "double"
DrawDamage(*dmg);
}
}
void DrawDamage(double damage) {
// ...
}
private:
js::Value monster_; // empty js::Value handle
};
Creating Values
You can create JavaScript values in C++ using js::Context:
js::Value settings = ctx.MakeObject();
settings["volume"] = 5;
settings["muted"] = false;
ctx["applySettings"](settings);
js::Value slots = ctx.MakeArray({ ctx.Make("slot1"), ctx.Make("slot2") });
js::Value defaults =
js::OrEmpty(ctx.MakeFromJSON(R"({"volume": 5, "muted": false})"));
ctx["defaults"] = defaults;
Call ctx.Make() to convert a C++ value into a js::Value. Use MakeObject() to create an empty object, MakeArray() to create an array, and MakeFromJSON() to parse a JSON string.
For the full list of supported types, see Passing Data Across the Bridge.
Type Inspection
You can inspect the underlying JavaScript type of a value with query methods:
js::Value loot = monster_["loot"];
if (loot.IsArray())
Use(loot);
if (monster_["boss"].Get().IsNullish()) // missing, undefined, or null
Log("not a boss");
Methods such as IsNumber(), IsString(), IsArray(), IsObject(), and IsCallable() check whether a value matches a specific type.
The IsNullish() method returns true if the value is either js::null or js::undefined.
Type Conversions
Strict Conversions
You can convert values to C++ types using Or(), Maybe<T>(), or To<T>():
double damage = monster_["damage"].Or(0.0); // 12
std::optional<double> armor = monster_["armor"].Maybe<double>(); // nullopt
js::Result<std::string> name = monster_["name"].To<std::string>();
These methods use strict type-checking— they require the JavaScript value to match the requested C++ type exactly. The string "7" does not convert to a number.
The three methods differ only in how they report failures. Or() returns a fallback value, Maybe<T>() returns std::nullopt, and To<T>() returns a js::Result<T> containing an error message.
You should use Or() for most property reads where you expect the handle to be valid and convertible.
JavaScript Conversions
To convert values using JavaScript's implicit conversion rules, you should instead call ToNumber(), ToString(), ToBoolean(), or ToJSON() directly on a js::Value:
js::Value level = monster_["level"]; // Raw value is "7" (string)
double strict = level.Or(0.0); // FAIL: 0.0 (strict)
double implicit = level.ToNumber().value_or(0.0); // SUCCESS: 7.0 (implicit)
Object Properties
Reading Properties
You can check for the existence of a property using Has() and read properties using operator[]:
// Check if "monster" has object property "boss"
if (monster_.Has("boss"))
Use(monster_["boss"]); // (won't be reached, doesn't exist)
// monster.boss doesn't exist (see code at top), so this falls back to "0.0"
// instead of throwing an exception.
double boss_level = monster_["boss"]["level"].Or(0.0);
Chained reads fail safely like JavaScript optional chaining, so reading a property on a missing object returns an empty js::Value instead of throwing an exception.
Writing Properties
Assign directly to a property to write to it:
monster_["name"] = "Vampire Lord";
monster_["damage"] = 20;
monster_["boss"] = js::null;
Native C++ values convert automatically on assignment. Failed writes are ignored silently (such as when a setter throws). To detect failures, use SetProperty(), which returns a js::Result with explicit error values.
Iteration
To iterate over an array or an object, convert it to a standard C++ container:
auto loot = js::OrEmpty(monster_["loot"].To<std::vector<js::Value>>());
for (js::Value& item : loot)
Log(item.Or("")); // "gold", then "fang"
auto fields = js::OrEmpty(monster_.To<std::map<std::string, js::Value>>());
Converting an array to std::vector<js::Value> lets you loop over its elements. Converting an object to std::map<std::string, js::Value> lets you walk its key-value pairs. In both cases, js::OrEmpty returns an empty container if the conversion fails.
Passing Values Between Pages
To pass data between pages, copy it to the destination context using JSON or native types:
// The page in another View (eg, an in-game bestiary):
js::Context bestiary(bestiary_view.get());
std::string json = monster_.ToJSON().value_or("null");
bestiary["monster"] = js::OrEmpty(bestiary.MakeFromJSON(json));
A js::Value belongs to the page that created it— native code can pass a handle directly only when both pages are frames of the same top-level page and share the exact same origin. Values from another View are always refused.
Any operation that receives a value from an incompatible page fails with a TypeError whose code is ULJS_CROSS_CONTEXT (see JavaScript Errors and Diagnostics).
Ownership Cycles
🚧 Circular References Prevent Garbage Collection
When a bound native class instance stores a
js::Valuepointing back to its own JavaScript object, neither can be garbage-collected while the page is alive. To break the cycle, clear the handle in an explicit close method or store ajs::WeakValueinstead (see Exposing Native Classes).
Weak Values
A js::WeakValue holds a reference to a JavaScript object without preventing garbage collection:
js::WeakValue cached(monster_);
// Later:
if (js::Value strong = cached.Lock())
UseCached(strong);
else
RebuildCache();
This non-owning reference is useful for observing JavaScript values without keeping them alive.
Call Lock() to obtain a valid js::Value for the object. If the object has been garbage-collected or its page navigated away, Lock() returns an empty handle.
🚧 JavaScript Callbacks Require Strong References
Never use
js::WeakValueas the only reference to a JavaScript callback. If nothing on the page holds the function, the garbage collector reclaims it.