docs

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
<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:

C++
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:

C++
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:

C++
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>():

C++
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:

C++
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[]:

C++
// 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:

C++
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:

C++
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:

C++
// 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::Value pointing 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 a js::WeakValue instead (see Exposing Native Classes).

Weak Values

A js::WeakValue holds a reference to a JavaScript object without preventing garbage collection:

C++
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::WeakValue as the only reference to a JavaScript callback. If nothing on the page holds the function, the garbage collector reclaims it.