Back to JavaScript
JavaScript
hard
mid

How would you traverse a deeply nested object and resolve the values of all functions inside it?

Recursively walk the structure; if a value is a function call it (with optional context), replace inline; if an array or object, recurse. Mind cycles with a `WeakSet`; mind async (await thenables) and class instances (don't recurse into them).

3 min read·~18 min to think through

Walk the input. For each value: function → call → use result; object/array → recurse; primitive → keep.

Sync version

js
function resolve(input, ctx) {
  if (input == null) return input;
  if (typeof input === "function") return input(ctx);

  if (Array.isArray(input)) {
    return input.map((v) => resolve(v, ctx));
  }

  if (isPlainObject(input)) {
    const out = {};
    for (const k of Object.keys(input)) out[k] = resolve(input[k], ctx);
    return out;
  }

  return input;     // primitives, class instances, Date, etc.
}

function isPlainObject(v) {
  if (v === null || typeof v !== "object") return false;
  const p = Object.getPrototypeOf(v);
  return p === null || p === Object.prototype;
}

Async version

js
async function resolveAsync(input, ctx) {
  if (input == null) return input;
  if (typeof input === "function") return await input(ctx);
  if (Array.isArray(input)) {
    return await Promise.all(input.map((v) => resolveAsync(v, ctx)));
  }
  if (isPlainObject(input)) {
    const entries = await Promise.all(
      Object.entries(input).map(async ([k, v]) => [k, await resolveAsync(v, ctx)]),
    );
    return Object.fromEntries(entries);
  }
  return input;
}

Promise.all parallelizes sibling work — same-depth functions resolve concurrently.

Cycles

If the input may self-reference (rare for config-like data but possible):

js
function resolve(input, ctx, seen = new WeakSet()) {
  if (typeof input === "object" && input !== null) {
    if (seen.has(input)) return input;   // or throw
    seen.add(input);
  }
  // ... same as above
}

Edge cases

  • Class instances (Date, Map, Set, custom classes) — don't recurse into them; treat as opaque.
  • Functions returning functions — decide policy (call once, or keep calling until non-function).
  • this binding — pass ctx and let callers use arrows, or call with a target context.
  • Mutability — return new objects/arrays; never mutate input.

Usage example

js
const config = {
  apiUrl: () => process.env.API_URL,
  user: {
    id: () => currentUser.id,
    name: "Static",
  },
  filters: [() => Date.now()],
};

resolve(config);
// { apiUrl: "https://...", user: { id: 42, name: "Static" }, filters: [1700000000] }

Interview framing

"Recursive walk: function → call, array/object → recurse, primitive → keep. isPlainObject guard prevents recursing into class instances (Date, Map). Async variant uses Promise.all to parallelize sibling work. Handle cycles with a WeakSet if the data might self-reference. Decide upfront whether to recursively resolve a function's return value or stop at one level — depends on the use case."

Follow-up questions

  • How would you handle async functions?
  • What's the policy if a function returns another function?
  • How would you support cycle detection?

Common mistakes

  • Recursing into class instances (Date/Map breaks).
  • Mutating input.
  • No async story — Promises returned by functions never awaited.

Performance considerations

  • O(n) in input size. Async version parallelizes sibling work. Deep nests can stack-overflow recursive impl — convert to iterative if user-controlled depth.

Edge cases

  • Cycles.
  • Symbol keys.
  • Getters that throw.
  • Sparse arrays.

Real-world examples

  • Config resolvers, GraphQL lazy fields, Storybook arg resolution, theme tokens that pull from CSS vars.

Senior engineer discussion

Seniors clarify policy questions upfront (class instances, async, cycles, fn-returning-fn) before coding. Those are the bugs that surface in production.

Related questions