# WASM Opaque objects VS \`for...in\`

**URL:** <https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443>\
**Category:** I have questions\
**Created:** [October 7, 2025, 3:30pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443 "2025-10-07T15:30:35Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 7, 2025, 3:30pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/1 "2025-10-07T15:30:35Z")

</div>

It looks like the most primitive thing that works since ECMAScript 3rd Edition or even before is broken with WebAssembly opaque objects and I wonder who should I bug about it.

```javascript
for (const k in ref);
TypeError: WebAssembly objects are opaque

```

This error has been “_fun_” to dig into but basically I’ve learned that a `for…in` over an opaque object could throw, as opposite of doing nothing, and I wonder why that would ever be the desired case … it’s literally something that has worked forever, it has no counter-utilities backed in able to crawl the prototypal inheritance, can we please amend this shenanigan that has been hard to dig into and absolutely unexpected?

A WebAssembly opaque object shows around as a random Object literal with a `null` prototype, and all operations work fine except the most primitive/simple one which is `for…in` and I believe a `for..in` should never, ever, throw on anything looking like an Object, acting like an object, showing `toString` as an Object, and so on … can we agree on this?

Thanks!

**edit** worth mentioning, also `postMessage` would drill into the fact that reference is a WebAssembly opaque object and fail at runtime to pass it around, as opposite of cloning it like `{\__proto_\_: null}` instead 🤦

**edit 2** also worth stating that `Object.keys(wasm)` or `Reflect.ownKeys(wasm)` don’t throw … why would `for…in` be that hostile instead of just ignoring opaque objects it cannot clearly `in` to?

---

<div class="post-metadata">

**Author:** ![ljharb](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/ljharb/32/8_2.png) [@ljharb](https://es.discourse.group/u/ljharb)\
**Post date:** [October 7, 2025, 3:46pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/2 "2025-10-07T15:46:47Z")

</div>

It's legal in JS for this to happen - for example, see `var o = new Proxy({}, { ownKeys() { throw 42 } }); for (var x in o) {}` - so it'd be up to the WebAssembly folks.

That said, my Proxy example _does_ throw for Object.keys and Reflect.ownKeys, so that does seem like an overly exotic object to me.

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 7, 2025, 3:58pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/3 "2025-10-07T15:58:40Z")

</div>

> [@ljharb](#):
>
> That said, my Proxy example _does_ throw for Object.keys and Reflect.ownKeys, so that does seem like an overly exotic object to me.

exactly … if you want to have a throwing Proxy that’s hostile with everything on purpose I am fine (and questioning you, as I don’t usually want to try/catch every line of my code) but in this case, the `console.log(wasm)` shows `{}` , the `getPrototypeOf(wasm)` shows `null`, the `Object.keys(wasm)` shows `[]` and so does `Reflect.ownKeys(wasm)` the `”string” in wasm` returns always `false`, everything is really working as expected, then a `for…in` would bail out the whole program in between, and so would `postMessage` for something impossible to detect and that has no meaning except being a “_literal-like_” reference around the JS world?

That’s a tad too far to me as “_exotic_” object and its meaning.

---

<div class="post-metadata">

**Author:** ![bakkot](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/bakkot/32/22_2.png) [@bakkot](https://es.discourse.group/u/bakkot)\
**Post date:** [October 7, 2025, 6:35pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/4 "2025-10-07T18:35:01Z")

</div>

I’m not sure that’s actually even legal per the JS spec. I guess maybe if `Object.getPrototypeOf(ref)` throws then that would be legal.

Can you provide a minimal reproduction? I don’t know what “ref” is supposed to be here. I can try to follow up in the WebAssembly spec if you can give a reproduction that doesn’t require libraries or anything.

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 7, 2025, 6:55pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/5 "2025-10-07T18:55:21Z")

</div>

> [@WebReflection](#):
>
> `WebAssembly objects are opaque`

the stack is overly-complex and the bug took a while for me to investigate and provide the source of _issue_ but basically it boils down to whatever in the engine will produce the quoted error … that is the use case I am after, but it’s been super hard for me to date to isolate that, the WASM env runs in a Web Worker, it passes through postMessage and before that, it passes through a serializer that tries to convert in a meaningful way things that are not supposed to be posted via postMessage and it failed in there … the key reference though, or the source of the issue, was a `Function(‘return 1‘)` that up to its invoke is fine, as reference, but once invoked it fails with such error.

More details within this issue I’ve filed to Pyodide stack but it’s unclear until now what would produce such reference that throws all over the place via `for…in` (or `postMessage`, but I am less concerned about the latter) … few comments of mine later you’ll see the workaround I had to push in order to grab latest Pyodide: [TypeError: WebAssembly objects are opaque · Issue #5929 · pyodide/pyodide](https://github.com/pyodide/pyodide/issues/5929#issuecomment-3377201964)

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 7, 2025, 7:01pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/6 "2025-10-07T19:01:06Z")

</div>

on the other hand … because the WASM reference is opaque, there is no way I can debug any further its nature but I start believing that recent WASM API changes might create function references out of the blue that result as `typeof “object“` on the JS side, specially if these come from evaluation or `Function(…)` invokes … that would explain the inability for me to understand the correct kind of thing that is traveling but it wouldn’t explain why my workaround out of the Proxy logic would make everything fine, as the Proxy handler differs between objects, arrays, and functions … as these are the only primitives/type that matters for proxies … yet that error wouldn’t tell me what is going on with that reference, what is that reference, or how to solve it … it looks like WASM done this way simply makes debugging impossible, which should be no environment goal/purpose, otherwise nobody can really provide robust code out there.

---

<div class="post-metadata">

**Author:** ![bakkot](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/bakkot/32/22_2.png) [@bakkot](https://es.discourse.group/u/bakkot)\
**Post date:** [October 7, 2025, 7:33pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/7 "2025-10-07T19:33:29Z")

</div>

We can’t really do anything useful here without a reproduction I’m afraid. I need to be able to produce one of these objects in order to say anything about it.

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 7, 2025, 9:00pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/8 "2025-10-07T21:00:16Z")

</div>

Yeah, makes sense … I was hoping for an outstanding long-time gotcha in the `for…in` specs but I’ve asked what _they_ are returning as WASM object, it’s also possible it’s them messing up with some internal WASM proxy but AFAIK there’s no such thing as internal WASM proxy, yet let’s see what’s their response: [TypeError: WebAssembly objects are opaque · Issue #5929 · pyodide/pyodide · GitHub](https://github.com/pyodide/pyodide/issues/5929#issuecomment-3378390096)

I hope I won’t be stuck in this back and forward loop as finding `for…in` is the cause of my issue was already a huge achievement to me

---

<div class="post-metadata">

**Author:** ![hoodmane](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/hoodmane/32/1778_2.png) [@hoodmane](https://es.discourse.group/u/hoodmane)\
**Post date:** [October 8, 2025, 2:12am UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/9 "2025-10-08T02:12:53Z")

</div>

The minimal reproducer is as follows:

```javascript
const {instance} = await WebAssembly.instantiate(new Uint8Array([
  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x02, 0x60,
  0x00, 0x01, 0x6f, 0x5f, 0x00, 0x03, 0x02, 0x01, 0x00, 0x07, 0x05, 0x01,
  0x01, 0x66, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0xfb, 0x01, 0x01,
  0xfb, 0x1b, 0x0b
]));
const x = instance.exports.f();
for (var a in x) {}

```

In the text format the WebAssembly here is:

```javascript
(module
  (type $empty_struct (struct))

  (func (export "f") (result externref)
    struct.new $empty_struct
    extern.convert_any
  )
)

```

I have no opinion on whether this is a problem or not, though I am very much looking forward to a JS API for interacting with wasm-gc objects.

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 8, 2025, 7:43am UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/10 "2025-10-08T07:43:36Z")

</div>

I don’t know if this would help anyone but apparently this code could be used to detect _exotic_ references:

```javascript
const { getPrototypeOf, isSealed, keys } = Object;

const isExotic = ref => !getPrototypeOf(ref) && isSealed(ref) && !keys(ref).length;

```

now … as this would likely be on any logic fast path that is trying to introspect objects as quickly as possible, I fully agree about this idea:

> I am very much looking forward to a JS API for interacting with wasm-gc objects.

I don’t know how many kind of exotic objects there are out there but a static native `Object.isExotic(ref):boolean` that would know out of the box if an object is exotic or not would definitively help libraries around the WASM / JS world to avoid runtime harakiri on surprising behaviors like the one mentioned in here.

Still, reading the specs, I believe that `for..in` should never throw and bail out/break right away instead of even creating the generator for properties.

**edit** no strong opinion around `Object.isExotic` VS `Object.isOpaque` … it’s the perf and guards needed natively I am after

---

<div class="post-metadata">

**Author:** ![mhofman](https://avatars.discourse-cdn.com/v4/letter/m/f14d63/32.png) [@mhofman](https://es.discourse.group/u/mhofman)\
**Post date:** [October 8, 2025, 8:16am UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/11 "2025-10-08T08:16:40Z")

</div>

> [@WebReflection](#):
>
> this code could be used to detect _exotic_ references:
> 
> ```javascript
> const { getPrototypeOf, isSealed, keys } = Object;
> 
> const isExotic = ref => !getPrototypeOf(ref) && isSealed(ref) && !keys(ref).length;
> 
> ```

No that only checks for a frozen/sealed object with a null proto and no keys, which you can just build as `Object.preventExtensions({ __proto__ : null})`

---

<div class="post-metadata">

**Author:** ![mhofman](https://avatars.discourse-cdn.com/v4/letter/m/f14d63/32.png) [@mhofman](https://es.discourse.group/u/mhofman)\
**Post date:** [October 8, 2025, 8:23am UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/12 "2025-10-08T08:23:58Z")

</div>

> [@WebReflection](#):
>
> a static native `Object.isExotic(ref):boolean` that would know out of the box if an object is exotic or not would definitively help

A language predicate for easily testing for exotic behavior (whether spec, Proxy or host) is a non starter. But it shouldn't be necessary as all objects including exotic ones are supposed to follow some invariants that make it possible to reason about code interacting with objects. Apparently these wasm opaque objects may be having some behavior that go against that spirit (but possibly not the letter of those invariants).

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 8, 2025, 12:51pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/13 "2025-10-08T12:51:01Z")

</div>

well, if that is decided to be the case, all arguments around “_you should not understand it’s an opaque object_” fades away with a `try/catch` over `for…in` if the `typeof ref` is `”object”` and it’s not `null` … my previous code was to avoid try/catching around but I really would like to not have these kind of surprises for “_ducks_” that look like literal and fail miserably for no reason whatsoever instead. Keep those “invisible” would be fine, current state is plain broken for interoperability of those objects around libraries.

```javascript
const isExotic = ref => {
  if (ref && typeof ref === 'object') {
    try { for (const k in ref) return false; }
    catch { return true; }
  }
  return false;
}

```

There a variant of the function that reveals those kind of objects.

---

<div class="post-metadata">

**Author:** ![Josh-Cena](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/josh-cena/32/1408_2.png) [@Josh-Cena](https://es.discourse.group/u/Josh-Cena)\
**Post date:** [October 8, 2025, 1:53pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/14 "2025-10-08T13:53:55Z")

</div>

This is a function called `keysAreNotTraversible`. “Exotic objects” have a very precise definition in the spec which requires at least one internal method to be overridden, but your function only detects one of such cases, which is [[OwnPropertyKeys]] being overridden. If you want to propose an `isExotic` function, it must also return `true` for an object that overrides [[Get]], one that overrides [[SetPrototypeOf]], etc. but I cannot imagine that to be useful. In fact, even `new Proxy({}, {})` would technically be exotic although it doesn’t have observable differences from an ordinary object. I think you may have better chances asking Wasm to add a `WebAssembly.isWasmObject()` predicate (like `Error.isError`) or them to make these objects less bizarre.

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 8, 2025, 2:14pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/15 "2025-10-08T14:14:15Z")

</div>

fair … I’ve thought there was somehow some collaboration between the two standards but of course this whole issue feels more related to the `WebAssembly` namespace … could anyone please help me out finding the right place to file this exact same issue so that we can eventually discuss this in there and explain that current state breaks entirely JS expectations around “_opaque objects_” as these mean troubles at runtime and are currently extremely hard to detect?

---

<div class="post-metadata">

**Author:** ![mhofman](https://avatars.discourse-cdn.com/v4/letter/m/f14d63/32.png) [@mhofman](https://es.discourse.group/u/mhofman)\
**Post date:** [October 8, 2025, 4:47pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/16 "2025-10-08T16:47:27Z")

</div>

> [@WebReflection](#):
>
> Keep those “invisible” would be fine, current state is plain broken for interoperability of those objects around libraries.

This is exactly the problem. The object invariants in JS are there so that code shouldn't have to worry about such things. I re-iterate, in most cases user code doesn't have to worry about exotic behavior (exceptions perhaps for exotic behavior causing re-entrancy). In this case your concern is legitimate, getting a Type error from interacting with these objects in a for-in statement is surprising, but only because the ownKeys and getPrototypeOf on that object don't result in that Type error.

> [@Josh-Cena](#):
>
> you may have better chances asking Wasm to add a `WebAssembly.isWasmObject()` predicate (like `Error.isError`) or them to make these objects less bizarre.

Such a predicate makes more sense to me, but likely not for the language itself, as I can't imagine a reason for these opaque objects to have this kind of exotic behavior. Unless they plan to natively expose properties on these objects in the future and prefer to throw when accessing own keys, in which case they should be consistent.

---

<div class="post-metadata">

**Author:** ![guybedford](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/guybedford/32/66_2.png) [@guybedford](https://es.discourse.group/u/guybedford)\
**Post date:** [October 8, 2025, 4:57pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/17 "2025-10-08T16:57:41Z")

</div>

WebAssembly [Custom Descriptors]([GitHub - WebAssembly/custom-descriptors: Custom RTTs and JS interop for Wasm GC structs](https://github.com/WebAssembly/custom-descriptors)) are a new WebAssembly proposal for providing WebAssembly structs like this with JS methods, static methods and constructors. They define and use an internal JS descriptor binary format to define the object descriptor.

For those in the committee interested in these interactions, since it has such a close link with JS interop, it would be well worth taking the time to read through that proposal in the explainer here - [custom-descriptors/proposals/custom-descriptors/Overview.md at e619f8c60dcd235097a5813dcd67e0043ebcb525 · WebAssembly/custom-descriptors · GitHub](https://github.com/WebAssembly/custom-descriptors/blob/e619f8c60dcd235097a5813dcd67e0043ebcb525/proposals/custom-descriptors/Overview.md).

I recently filed this issue for supporting symbol method definitions for custom descriptors as well as normal names - [Defining Symbol.dispose and other symbol methods · Issue #60 · WebAssembly/custom-descriptors · GitHub](https://github.com/WebAssembly/custom-descriptors/issues/60), where it seems like `Symbol.iterator` support might help with the root issue posted here.

---

<div class="post-metadata">

**Author:** ![bakkot](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/bakkot/32/22_2.png) [@bakkot](https://es.discourse.group/u/bakkot)\
**Post date:** [October 8, 2025, 5:07pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/18 "2025-10-08T17:07:15Z")

</div>

> [@WebReflection](#):
>
> could anyone please help me out finding the right place to file this exact same issue

The right place for spec issues of this kind would be the [WebAssembly specification](https://github.com/WebAssembly/spec); specifically you would want a JS-API issue. However, checking the spec, [exported GC objects like the one in the reproducer are defined with the normal MOP operations](https://webassembly.github.io/spec/js-api/#gc-exotic-objects), and the `[[OwnPropertyKeys]]` method just returns an empty list. Which isn’t consistent with throwing for `for-in`.

Also, the reproducer above doesn’t actually reproduce on Safari or Firefox for me, just for Chrome. So in this case, it’s not an issue with the specs: it’s just a Chrome bug. As such the right place is the Chrome bug tracker. You could also consider adding a [Web Platform Test](https://github.com/web-platform-tests/wpt/tree/4657034a3092367d86fbc3bccdc4e21d6482a34b/wasm/jsapi/gc) for this case, which will encourage browsers to get it right.

---

<div class="post-metadata">

**Author:** ![mhofman](https://avatars.discourse-cdn.com/v4/letter/m/f14d63/32.png) [@mhofman](https://es.discourse.group/u/mhofman)\
**Post date:** [October 8, 2025, 6:05pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/19 "2025-10-08T18:05:33Z")

</div>

I believe the enumeration error is coming from [here](https://github.com/v8/v8/blob/80ddedfe2c4b798a82127c72b907be20f0e07a8d/src/objects/keys.cc#L275-L276), there is a special check for wasm objects, which shouldn't be necessary if the prototype is `null`, or at least should emulate the behavior of a null prototype object.

All the other mutating [MOP operations](https://github.com/v8/v8/blob/80ddedfe2c4b798a82127c72b907be20f0e07a8d/src/objects/js-objects.cc) seem to throw as well for wasm objects, which is likely unnecessary too since these objects appear as sealed/frozen in the first place.

For example the following throws which is surprising for any object that attempts to follow ordinary semantics: `Reflect.isExtensible(x) || Reflect.preventExtensions(x)` (preventing extensions on an already non-extensible object should not throw, it should be no-op).

Edit: this seems to be a quite ancient behavior in v8, [implemented 3 years ago](https://github.com/v8/v8/commit/4893b1c0bd8a5eb94921a691c1f02c555c5c565a).

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [October 8, 2025, 6:16pm UTC](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443/20 "2025-10-08T18:16:56Z")

</div>

I am both grateful for so much context provided and yet sad I’ve still no idea what’s going on:

- is this “just” a Chromium bug? I’ve read some version of NodeJS doesn’t throw, so I think this has been fixed in most recent v8 implementation?
- is this spec’d as “do whatever”? In such case I believe it should be amended, as clearly consistency around vendors matters more than such “whatever” that could poison or fail JS code expectations
- is this … “you name it” ? Please let me know what/where exactly this bug/issue should exist and I’ll take care of the rest (as best as I can do)

Thank you!

[Next page](https://es.discourse.group/t/wasm-opaque-objects-vs-for-in/2443.md?page=2)
