# Implementing Await Dictionary in SpiderMonkey This all started with my curiosity to try new things and understand how things work behind the scenes, and with working on a project that serves millions of users worldwide. I joined Mozilla in late September, set up my development environment, and by the start of October, I had already found easy bugs to fix, and that's how I got into contributing to the component. I later checked the blogs and found out that one of the previous interns had implemented the [Iterator.range](https://spidermonkey.dev/blog/2025/03/05/iterator-range.html) proposal, and this interested me. I reached out to the mentor, Daniel, for a similar project, and luckily there was one, [Import Bytes](https://bugzilla.mozilla.org/show_bug.cgi?id=1996572), that had just reached stage 2.7 and was ready for implementation. I took it up and started it, but there was a blocker and we had to wait for that to be solved. Story for another blog, right? [The Await Dictionary](http://bugzilla.mozilla.org/show_bug.cgi?id=2003342) was there, just ready after the first blocker. Daniel had suggested I do this as we waited. Due to his time commitment, he suggested we find another mentor for me for this project. Arai has always been there since I started contributing, and I had solved the first bugs with their help. I suggested he guide me, and that's how we started the journey on implementation. ## Understanding the proposal The Await Dictionary introduces keyed Promise combinators: `Promise.allKeyed(promises)` `Promise.allSettledKeyed(promises)` Instead of consuming [iterables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) and returning arrays, these APIs consume an iterable and return an object keyed by the original property names. `Promise.all` and `Promise.allSettled` are excellent for arrays, but most real application state is often object-shaped. Converting to arrays loses naming context and forces remapping. `Await Dictionary` preserves those keys end-to-end. Let's look at this example: ```javascript= const result = await Promise.allKeyed({ user: fetchUsers(), settings: fetchSettings(), stats: fetchStats(), }) // You can safely use it this way: // result.user, result.settings, result.stats ``` For `allSettledKeyed`, each key maps to `{ status, value }` or `{ status, reason }`. ## Implementation strategy in SpiderMonkey The SpiderMonkey already had mature implementations for `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any`. So for [Await Dictionary](https://github.com/tc39/proposal-await-dictionary), I did not build a new combinator pipeline from scratch. Instead, I added the keyed variants and reused the existing combinator structure where possible. ### CreateKeyedPromiseCombinatorResultObject(keys, values) The proposal operation `CreateKeyedPromiseCombinatorResultObject(keys, values)` is a helper that creates a plain object and defines properties for each key/value pair. Talking about the Keyed data path, `CreateKeyedPromiseCombinatorResultObject`, I had to extended [`PromiseCombinatorKeyedDataHolder`](https://searchfox.org/firefox-main/source/js/src/builtin/Promise.cpp#240) to store an extra `Slot_KeysList` that stores the parallel key list so we can finally resolve into an object with the original keys. In the spec, those are tracked as keys and values, both described as Lists. In SpiderMonkey, that's an internal engine data structures used to model the spec’s abstract Lists. In this implementation, those lists are represented with `ListObject`, which is SpiderMonkey’s internal representation for spec-style list data that lives inside the engine rather than in user-visible JavaScript. Conceptually, the helper take two parallel lists, walk them in order, and define each property on a result object. The result object is created as a plain object with a null prototype as in step 2 of the spec. JavaScript is prototype-based, which means objects normally inherit behavior through a prototype chain. **JavaScript is prototype-based**, so objects normally inherit properties through a prototype chain. For example: ```javascript const base = { greet() { return "hi"; } }; const obj = Object.create(base); obj.greet(); // output: "hi" ``` Even **class** is mostly syntax sugar over that prototype system, with methods placed on prototype. [Read more about it here on classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain). We're not inheriting properties at this point, as we just want a clean dictionary-like result containing only the original keys, so we createa the object with a null prototype. ```javascript= // Step 2. Let obj be OrdinaryObjectCreate(null). JS::Rooted<PlainObject*> obj(cx, NewPlainObjectWithProto(cx, nullptr)); if (!obj) { return nullptr; } ``` For each entry in the internal keys and values lists, SpiderMonkey converts the stored key back into its internal PropertyKey type using [ToPropertyKey](https://searchfox.org/firefox-main/source/js/src/vm/JSObject-inl.h#292) before defining the result object property. This conversion is necessary because the temporary ListObject stores generic JS values, but property definition requires PropertyKey. Since the keys originated from the object's own properties via [[OwnPropertyKeys]], the conversion is internally consistent and completes successfully. We're also defining the property with the property with `JSPROP_ENUMERATE`, which sets [[[Enumerable]]](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Enumerability_and_ownership_of_properties) to true. That means the resulting properties behave like normal object properties and show up in things like `Object.keys`. ```javascript= const result = {}; Object.defineProperty(result, "user", { value: 1, enumerable: true, }); Object.keys(result); // ["user"] ``` ### Static method declarations and entry points The public entry points here; `Promise_static_allKeyed` and `Promise_static_allKeyed` delegates to share keyed entry path `CommonPromiseCombinatorKeyed`. That keyed wrapper does the same high-level static-method work as the iterable family (`Promise.all`, `Promise.allSettled`, `Promise.any`, `Promise.race`): validate `this`, create a promise capability, compute promiseResolve, and call a perform function. The difference is what gets initialized and iterated: keyed variants initialize a promises object instead of a `PromiseForOfIterator`. #### The two keyed variants in code The core variant split is done by choosing which perform function to pass; `Promise_static_allKeyed` -> `PerformPromiseAllKeyed` and `Promise_static_allSettledKeyed` -> `PerformPromiseAllSettledKeyed` Both are implemented through one shared pipeline `CommonPerformPromiseKeyedCombinator` which handles property enumeration, key/value list tracking, remaining element bookkeeping, and final resolve. So, the variant-specific behavior is injected through callbacks that create element functions and define how each settled value is stored. For **allKeyed**, the fulfillment element function stores raw values. For **allSettledKeyed**, fulfillment/rejection element functions store structured records: ``` { status: "fulfilled", value } { status: "rejected", reason } ``` ### The challenging part: refactoring to share machinery Here, the challenging part I encoutered, was to refactor the existing combinator internals so keyed/property-based and iterator-based variants could share one robust path without breaking the existing implementation. At first I did not fully see the optimization picture. I understood behavior, but not all the performance and observability constraints around side effects, wrappers, and built-in fast paths. C++ being new to me made this harder, especially with templates, loop structure, and callback-style specialization in engine code. I discussed it with my mentor, broke the work into smaller refactoring steps, and iterated from a prototype toward a cleaner shared implementation. My first draft got 184 comments, which was painful but incredibly educational. I’ll link that first revision here: [first revision draft](phabricator.services.mozilla.com/D277479). ### Side effects and optimization strategy