#6612·zod

Async tuple rest transforms overwrite each other at the final index

Author: AaroniusCreated Sep 16, 2026Updated Sep 16, 2026

When a tuple has multiple rest elements parsed asynchronously, their results can all be written to the final tuple index.

Each asynchronous callback uses the same rest-element index. By the time the promises resolve, that index points to the final position. Whichever promise resolves last overwrites that position, while earlier positions remain empty.

Reproduction

import * as z from "zod/v4";

const schema = z.tuple([z.string()]).rest(
  z.string().transform(async (value) => {
    // Make "b" finish after "c".
    await new Promise((resolve) =>
      setTimeout(resolve, value === "b" ? 20 : 0)
    );

    return value.toUpperCase();
  })
);

console.log(await schema.parseAsync(["a", "b", "c"]));

Actual result

[ "a", <1 empty item>, "B" ]

The element at index 1 is a sparse-array hole. "B" appears at the final index because its promise resolves last and overwrites "C".

The timeout in the reproduction demonstrates that changing completion order changes which value survives. In the example, B survives at the last index. Without the setTimeout, C would survive at the last index, but the element at index 1 would still be a sparse-array hole.

Expected result

[ "a", "B", "C" ]

Each asynchronous rest result should be written to the index belonging to its input element, regardless of the order in which the promises resolve.

I have the fix in a branch on my fork. I'd be happy to open a PR if you'd like (I don't have rights to do so currently).