Frontend developer reviewing JavaScript sparse-array tests and required-slot validation on a desktop monitor.

Array.every Passed Without Checking Every Required Slot

Fri, Oct 16, 2026

Array.every Passed Without Checking Every Required Slot

new Array(3).every(predicate) can return true while calling predicate zero times. That result is not a bug in JavaScript. It follows the ECMAScript contract for Array.prototype.every: the method checks whether each indexed property exists before invoking the callback, skips an absent property, stops when a callback result converts to false, and returns true if no visited element fails. These statements assume a callable predicate and normal completion. The empty-array case also returns true.

That behavior becomes dangerous only when an application asks a stronger question than every answers. The record contract in this article is intentionally strict: the input must be a non-Proxy Array with length === 3; indices 0, 1, and 2 must each be the array's own data properties; and every stored value must be a finite integer from 1 through 9 inclusive. Extra named properties do not contribute required slots. An inherited numeric property does not satisfy ownership. An accessor does not satisfy the data-property rule. A hole is not equivalent to an own property whose value is undefined.

The engineering question is therefore not, “Did every callback invocation return true?” It is, “Were all three required own slots present, safe to inspect under the declared trust boundary, and valid?” We will build a deterministic Node.js 22.16.0 fixture that records what every actually visits, then replace the admission test with an explicit descriptor-based gate. The goal is semantic admission with preserved evidence, not a cleverer callback.

Define the three required own slots

Start with the business contract, because JavaScript's generic collection semantics are broader than the record semantics we need. The accepted shape is exactly an Array whose length is 3. Required indices are the string property keys '0', '1', and '2'. Each must resolve to an own property descriptor on the array itself, and that descriptor must be a data descriptor carrying a value. That value must satisfy:

Number.isInteger(v) && v >= 1 && v <= 9

Number.isInteger excludes NaN, positive and negative infinity, non-integral Number values, strings, boxed numbers, and other non-number values. The range check then narrows the admissible represented integers to 1–9. Nothing about a property named extra, label, or sourceId can compensate for a missing required numeric slot. A fixed record is defined by the designated positions, not by a count of enumerable own keys.

This is application policy, not a general statement about what counts as an Array element in ECMAScript. The Array.prototype.every algorithm is intentionally generic and bases callback execution on HasProperty, not on “own data property exists.” It can therefore skip an absent property or inspect an inherited numeric property. The distinction is central: our validator enforces a narrower contract than the language primitive.

The trust boundary is also deliberate. We test standard Array objects with synchronous primitive values, an unchanged global Array.prototype, and one local prototype used only for the inherited-index control. Proxies are excluded, including proxies anywhere in the inspected prototype chain. So are hostile replacement of built-ins, cross-realm assumptions, concurrent mutation, TypedArray semantics, asynchronous predicates, and hostile serialization. A production boundary that must defend against those threats needs a different design; this routine does not pretend to sandbox them.

The neighboring issue of stateful regex validation in JavaScript is useful as a contrast: there, the predicate's hidden state can alter repeated outcomes; here, the predicate is pure and the defect is population coverage.

Pin Node.js and keep the fixture side-effect controlled

The execution baseline for this fixture is Node.js v22.16.0, with V8 12.4.254.21-node.26 on Linux x64. The complete implementation was independently run under that baseline. The harness uses CommonJS and require('node:assert/strict'). The Node.js v22.17.0 assertion manual documents strict comparison semantics, including deepEqual behaving as deepStrictEqual in strict mode. That manual is a test-tool reference; the execution results reported here remain specifically from v22.16.0. Assertions belong in the fixture oracle, not in the production-facing validation API.

The TC39 pages cited here are living specification pages, consulted on October 16, 2026. Their edition label and this access date are not launch dates for every's long-established behavior. They provide the normative language contract; the pinned Node version provides an execution baseline. Multiple TC39 reference pages are sections of one specification, not independent studies.

The business predicate is pure. The diagnostic callback additionally appends each visited index to a local evidence array; it does not mutate the candidate array, await anything, touch the DOM, perform network I/O, or modify a shared prototype. The DELETED case performs its deletion before validation starts. The iteration range is established before callbacks begin, and mutation during traversal has additional semantics we do not need for this problem.

We call Array.prototype.every.call(array, callback) instead of array.every(callback). That prevents a fixture with an own property named every from accidentally changing which function is under test. It also makes the laboratory's subject explicit: the built-in method from the unchanged global prototype.

The admission gate is likewise kept narrow. It first rejects anything for which Array.isArray(input) is false, so both null and an array-like object such as {0: 2, 1: 4, 2: 6, length: 3} are rejected as type controls. It then requires length === 3. Only after those checks does it inspect the three required own property descriptors. Within the declared non-Proxy boundary, Object.getOwnPropertyDescriptor lets us determine whether a slot is missing and whether an own property is a data property without evaluating an accessor getter.

That last point is a preconditioned claim, not a universal security guarantee. Proxy descriptor operations can run user-supplied traps. We explicitly do not attempt to identify or sandbox arbitrary Proxy inputs, and Array.isArray is not a non-Proxy certificate.

A second control is global cleanliness. Nothing in the fixture writes to Array.prototype, Object.prototype, or any other shared prototype. The inherited-index experiment uses a prototype created with Object.create(Array.prototype) and installs property '1' only on that local object before assigning it to one fixture instance. This prevents the test from changing the semantics of unrelated arrays in the same process. It also keeps the regression deterministic: a dense record does not accidentally inherit laboratory data, and a later test cannot pass because an earlier test polluted global state.

That isolation is worth preserving in shared-library test suites. Sparse-array bugs are already about invisible structure; a harness that mutates global prototypes adds a second invisible variable. Fresh factories and per-instance setup make each row responsible for its own evidence. The repeat run later in the file is not just repetition for its own sake: it checks that rebuilding the fixture reproduces the same callback ledger and reason codes without depending on retained objects from the first pass.

With Node.js 22.16.0 installed, run the complete file from the regression section using these commands. No third-party package installation is required:

node --version
node sparse-slot-validation.cjs

Publish the fixture before implementing the repair

A regression test is stronger when expected evidence is authored independently of the implementation under repair. The source definitions and matrix below form the literal oracle for the nine main rows. “Visited” gives the expected callback indices, subsequently confirmed by the complete execution. “Own 0/1/2” records Object.hasOwn(array, String(i)); T and F mean true and false. Status and failing index are application decisions, not every results. The full executable factories appear in the complete CommonJS file below.

ID

Source construction

DENSE

[2,4,6]

ALL_HOLES

new Array(3)

ONE_HOLE

[2,,6]

UNDEFINED

[2,undefined,6]

EMPTY

[]

ZERO

[2,0,6]

INHERITED

[2,,6], with an instance-only prototype owning '1': 4

DELETED

[2,4,6], then delete array[1] before validation

NAMED

[2,,6], then array.extra = 4

Literal expected callback, ownership, and admission evidence:

ID

Length

every

Visited

Own 0/1/2

Status

Index

DENSE

3

true

[0,1,2]

T,T,T

ACCEPT

null

ALL_HOLES

3

true

[]

F,F,F

REJECT_MISSING_SLOT

0

ONE_HOLE

3

true

[0,2]

T,F,T

REJECT_MISSING_SLOT

1

UNDEFINED

3

false

[0,1]

T,T,T

REJECT_VALUE

1

EMPTY

0

true

[]

F,F,F

REJECT_LENGTH

null

ZERO

3

false

[0,1]

T,T,T

REJECT_VALUE

1

INHERITED

3

true

[0,1,2]

T,F,T

REJECT_MISSING_SLOT

1

DELETED

3

true

[0,2]

T,F,T

REJECT_MISSING_SLOT

1

NAMED

3

true

[0,2]

T,F,T

REJECT_MISSING_SLOT

1

The table contains two different kinds of false confidence. ALL_HOLES, ONE_HOLE, DELETED, and NAMED return true because missing positions do not invoke the predicate. INHERITED returns true for the opposite reason: index 1 is not an own property, but HasProperty finds it through the prototype chain, so the callback does run with value 4. Both behaviors are correct for every; neither proves our own-slot contract.

UNDEFINED and ZERO are equally important negative controls. They prove that an own slot with an invalid value is not the same condition as a missing slot. Because the predicate returns false at index 1, every short-circuits there, so index 2 is never visited. Any test expecting three callback invocations for every length-3 input would itself be wrong. The normative algorithm returns false as soon as a callback result coerces to false.

We add three controls outside the nine-row naive matrix. ACCESSOR starts from [2,4,6] and replaces property '1' with a getter that increments a counter and returns 4. We do not run naive every on this row, because that would execute the getter and defeat the no-read control. The repaired gate must return REJECT_ACCESSOR@1 while the counter remains zero. null and the array-like object are separate REJECT_TYPE cases.

Separate length from required-position coverage

Length is necessary but not sufficient. ALL_HOLES has length 3 with no required own properties at all. EMPTY has no slots and length 0, so the repaired gate rejects it at the length boundary before inspecting indexed descriptors. NAMED has length 3 and exactly three enumerable own keys, but those keys are '0', '2', and 'extra'. Required index '1' is still absent.

This is why neither array.length === 3 nor Object.keys(array).length === 3 answers the record question. A count collapses identity. The contract requires three particular own positions, so the validator must preserve and test those positions explicitly.

Observe every skipping holes without reporting failure

The first execution group is deliberately unsurprising once the language rule is visible. DENSE invokes the callback at 0, 1, and 2; all three values pass; every returns true. ALL_HOLES invokes no callback at all and still returns true. ONE_HOLE invokes 0 and 2, skipping 1; both visited values pass, so the result is true. DELETED begins dense, but deletion before validation removes the own property at index 1 while retaining length === 3; it therefore behaves like ONE_HOLE during iteration.

Observed excerpt from the complete program under Node.js v22.16.0 and V8 12.4.254.21-node.26:

DENSE      every=true  visited=[0,1,2] length=3 own=[true,true,true] gate=ACCEPT
ALL_HOLES  every=true  visited=[] length=3 own=[false,false,false] gate=REJECT_MISSING_SLOT@0
ONE_HOLE   every=true  visited=[0,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
DELETED    every=true  visited=[0,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1

The important step in the ECMAScript algorithm is the existence test before callback invocation. For each numeric key, every performs HasProperty; only when that result is true does it Get the value and call the predicate. If the property is absent, the loop advances silently. On normal completion, the final return is true if no callback result converted to false. A true Boolean therefore means “no callback that ran failed,” not “the callback ran for every integer from zero through length minus one.”

This is the exact gap fixed-length record maintainers need to internalize. The predicate may be perfect and still never receive malformed missing data. Validation coverage is a property of the iteration contract, not merely of the predicate body.

Distinguish a missing property from own undefined

Compare ONE_HOLE with UNDEFINED. Both may look “empty” when printed casually, but their property evidence differs. ONE_HOLE has no own descriptor for '1'; Object.hasOwn(array, '1') is false; every skips the index; and the naive result is true. UNDEFINED does have an own data property at '1' whose value is undefined; Object.hasOwn is true; every calls the predicate; the predicate returns false; and traversal stops at index 1.

The repaired gate rejects both, but for different reasons: REJECT_MISSING_SLOT@1 versus REJECT_VALUE@1. Keeping those reason codes separate makes producer diagnosis much easier. One says the record did not carry the required position. The other says the position existed but carried a value outside the domain. Conflating them destroys evidence that may determine which upstream component should be repaired.

Read HasProperty rather than assuming own-only iteration

A common shorthand says that every “skips holes.” That is useful but incomplete. The algorithm does not ask whether the array has an own property at the index. It asks HasProperty. ECMAScript defines HasProperty as an operation whose result may be satisfied by either an own or inherited property, while HasOwnProperty is the separate own-only operation.

The INHERITED fixture makes that difference executable without contaminating global state:

function makeInherited() {
  const array = [2, , 6];
  const localPrototype = Object.create(Array.prototype);
  Object.defineProperty(localPrototype, '1', { value: 4 });
  Object.setPrototypeOf(array, localPrototype);
  return array;
}

The array itself still has no own property '1'. Its local prototype, however, owns '1' with value 4. every checks index 1 with HasProperty, sees the inherited property, reads it, calls the predicate, and therefore visits [0,1,2]. All three values are valid and the naive Boolean is true. The repaired gate rejects index 1 because our business contract requires the array's own data property.

That fixture is a control, not a recommendation to encode records through prototype inheritance. It proves why an own-slot validator cannot infer ownership from callback coverage alone. The language mechanism is working as specified; the application policy is intentionally stricter.

A related collection-ownership topic is frozen Map entry mutation. It is adjacent rather than evidentiary here: this article does not reuse Map mutability as an explanation for Array index ownership.

Object.hasOwn is the language-level convenience for own-property membership: the specification converts the key and delegates to the own-property abstract operation. For the repaired gate, however, a descriptor gives us both ownership and the crucial data-versus-accessor distinction in one inspection step.

Inspect own descriptors before reading values

The production-facing validator should answer the declared contract directly. That means no callback-population ambiguity and no read of a required slot's value before its descriptor has been classified. The core routine is small:

function isBusinessValue(v) {
  return Number.isInteger(v) && v >= 1 && v <= 9;
}

function validateRecord(input) {
  if (!Array.isArray(input)) {
    return { status: 'REJECT_TYPE', index: null };
  }
  if (input.length !== 3) {
    return { status: 'REJECT_LENGTH', index: null };
  }

  for (let i = 0; i < 3; i += 1) {
    const descriptor = Object.getOwnPropertyDescriptor(input, String(i));

    if (descriptor === undefined) {
      return { status: 'REJECT_MISSING_SLOT', index: i };
    }
    if (!Object.hasOwn(descriptor, 'value')) {
      return { status: 'REJECT_ACCESSOR', index: i };
    }
    if (!isBusinessValue(descriptor.value)) {
      return { status: 'REJECT_VALUE', index: i };
    }
  }

  return { status: 'ACCEPT', index: null };
}

The order is intentional. Type is checked before length so array-like objects are not accepted by shape. Length is checked before descriptors because no length other than 3 can satisfy the record contract. Each required index is then inspected with Object.getOwnPropertyDescriptor, which returns undefined when the own property is missing. If the descriptor exists, Object.hasOwn(descriptor, 'value') identifies the data-descriptor shape we require. Only then do we examine descriptor.value.

This is clearer than calling Object.hasOwn(input, key) and then separately reading input[i]. The descriptor supplies the admission evidence: an own property exists, its kind is data rather than accessor, and its stored value is available without invoking a getter. Object.hasOwn documents the ownership check. The FromPropertyDescriptor algorithm explains the returned descriptor object's own value field for a data property, including when the stored value itself is undefined.

The function returns structured status rather than throwing an assertion. That keeps operational policy separate from the regression harness. A caller can record, reject, route, or hold a malformed record without translating test failures into control flow.

The loop returns the first failure in ascending index order. Rejection therefore does not mean that all later positions were evaluated by the gate. A separate dense slot ledger records indices 0, 1, and 2 even after a decision can already be made. Acceptance is stronger: execution reaches ACCEPT only after all three required descriptors and stored values pass.

Reject accessors without evaluating their getters

The accessor control makes the “inspect before read” rule visible. The result comments below were independently confirmed under the pinned Node.js baseline:

const counter = { count: 0 };
const array = [2, 4, 6];
Object.defineProperty(array, '1', {
  configurable: true,
  enumerable: true,
  get() {
    counter.count += 1;
    return 4;
  },
});

const result = validateRecord(array);
// result: { status: 'REJECT_ACCESSOR', index: 1 }
// counter.count: 0

The complete test run observed getterCalls=0. That claim is limited to this standard-Array, non-Proxy fixture. A Proxy can trap descriptor inspection and is outside the declared trust boundary. The gate therefore does not claim to detect all side effects or certify arbitrary hostile inputs. It avoids executing the own accessor getter in the environment actually tested.

Qualify all accepted and rejected outcomes

Once the validator expresses the business contract directly, only DENSE is admitted among the nine main rows. That is not because DENSE is “dense enough” in an abstract sense; it is because all three designated own descriptors are data properties and each stored value passes the domain predicate.

The main-row admission decisions, independently reproduced under the pinned baseline, are:

ID

Admission decision

Why

DENSE

ACCEPT

Three required own data slots; values 2, 4, 6 valid

ALL_HOLES

REJECT_MISSING_SLOT@0

First required own descriptor absent

ONE_HOLE

REJECT_MISSING_SLOT@1

Required own descriptor 1 absent

UNDEFINED

REJECT_VALUE@1

Own data slot exists; value invalid

EMPTY

REJECT_LENGTH

Length is 0, not 3

ZERO

REJECT_VALUE@1

Own data slot exists; 0 is outside 1–9

INHERITED

REJECT_MISSING_SLOT@1

Index 1 exists only via prototype

DELETED

REJECT_MISSING_SLOT@1

Deletion removed required own slot

NAMED

REJECT_MISSING_SLOT@1

Named property does not replace required index

The additional controls qualify what the gate means. ACCESSOR is REJECT_ACCESSOR@1, not a value rejection, because the contract forbids accessors regardless of what the getter would return. null is REJECT_TYPE. The plain array-like object with numeric keys and length: 3 is also REJECT_TYPE, because the contract says Array, not merely array-like.

Notice what the gate does not say. It does not claim the input came from a trustworthy producer. It does not verify record identity, authorization, schema version, or transport integrity. It does not prove a Proxy is absent through introspection. It does not establish immutability after the function returns. Acceptance means only that, at the synchronous admission instant and within the stated boundary, the object satisfies this fixed structural and numeric contract.

That scope discipline matters for API design. A status named ACCEPT is useful only if its subject is clear. Here it means “accepted as this three-slot record,” not “safe for every later use.” A caller that needs an independent snapshot can copy the three admitted primitive values under a separate policy; copying alone does not make that copy immutable. A caller that requires provenance can attach source authentication under a separate policy. Keeping those concerns separate prevents this tiny validator from accumulating claims it cannot prove and lets the regression suite remain focused on the one semantic boundary that motivated it.

Check reason codes and failing indices

Regression tests should assert the category and index, not just a generic false result. A change that turns REJECT_ACCESSOR@1 into REJECT_VALUE@1 may still reject the input while violating the required descriptor-kind decision. The getter counter separately detects an unwanted read, including a read that still returns the expected status. A change that reports REJECT_MISSING_SLOT@0 for ONE_HOLE would indicate a traversal or classification error. Exact reason codes and failing indices are part of the shared-library contract.

The fixture uses node:assert/strict to compare literal expected records with actual diagnostics and gate results. The production API returns plain objects such as {status, index}. Assertions fail the test harness when implementation behavior diverges, while reasoned return values let application code make explicit decisions without treating malformed user data as a test-framework exception. Strict assertion mode is versioned test tooling, not a second source of ECMAScript's iteration semantics.

Expose the named-property count counterexample

NAMED is the compact rebuttal to “just count the keys.” Its construction is:

const array = [2, , 6];
array.extra = 4;

Under the observed fixture, Object.keys(array) is ['0', '2', 'extra'], so Object.keys(array).length === 3. Yet Object.hasOwn(array, '1') === false, and naive every visits only [0,2]. The repaired gate correctly returns REJECT_MISSING_SLOT@1.

The mistake in key-count validation is not specific to enumeration order. It is a mismatch between a cardinality question and an identity question. “Are there three enumerable own string keys?” does not imply “Are the three required numeric keys present?” Extra named properties are outside this business decision. They may be useful metadata, or they may be ignored entirely, but they cannot be counted as substitute slots.

A maintainable validator should make that rule legible. An explicit loop over 0, 1, and 2 does more than avoid a sparse-array trap: it encodes which fields matter. That becomes particularly valuable when someone later adds recordId, debug, or another named property. A count-based shortcut changes meaning as object shape evolves; an indexed contract remains stable unless the contract itself changes.

The same principle applies to evidence. Preserve each record's identifier and each required index. A report saying “two callbacks succeeded” is not enough to tell whether indices 0 and 2 were checked, whether index 1 was inherited, or whether early termination hid index 2. Counts are summaries; admission needs positional facts.

Conversely, the contract does not prohibit named metadata. An otherwise conforming record is not invalid merely because extra exists. The gate never counts or reads such named properties. Enumeration flags are also outside the declared policy: requiring own data properties is not the same as requiring enumerable, writable, or configurable properties.

Preserve evidence before a JSON round trip

Serialization can erase exactly the distinction under investigation. In the clean fixture, these three expressions all produce the same JSON text:

JSON.stringify([2, , 6])
JSON.stringify([2, undefined, 6])
JSON.stringify([2, null, 6])

Observed JSON text for all three under Node.js v22.16.0:

[2,null,6]

This follows the array serialization algorithm: indices from zero through length - 1 are serialized in order, and when serialization of an array entry yields undefined, the algorithm appends the text null. Non-index named properties are excluded from the serialized array representation. In this controlled case, a hole and an own undefined value therefore collapse to the same text as an actual null value.

That makes source-array JSON a poor forensic oracle for original slot presence. Once only [2,null,6] survives, you cannot infer whether the producer had a hole, stored undefined, or stored null. The right approach is to build a separate explicit ledger before lossy serialization. For each required index, record facts such as own, presence, kind, and a safe value classification. For example, the fixture distinguishes absent, inherited, own/data/undefined, own/data/null, and own/accessor without invoking the accessor getter.

The fixture's ledger can itself be serialized because its distinctions are explicit data rather than implicit object-shape semantics. Its JSON round trip is asserted in the complete file. Retain a record ID with the ledger so a later repair can request the correct upstream source rather than “fixing” an anonymous row.

The ledger retains finite numeric values and explicit type labels. Its null placeholders must be interpreted together with presence, kind, and valueKind; they are not claims that the source stored null. Inherited positions and accessors are marked unread. This is a diagnostic representation for admission, not a universal object snapshot or a generic serializer for arbitrary values.

Do not call densification a reconstruction of missing values

Spread and Array.from can materialize holes as own undefined entries in this fixture. After const dense = [...[2,,6]], index 1 exists as an own property whose value is undefined; the same is true for Array.from([2,,6]) in the tested environment. That transformation may be useful for some APIs, but it does not recover the missing business value.

A materialized undefined is evidence of a transformation, not evidence that the producer meant a particular integer. It still fails the 1–9 contract. Calling such a step “repair” would confuse shape normalization with semantic reconstruction. The authorized repair path is to obtain or regenerate the missing source value, not to densify and pretend completeness has been restored.

Place the admission gate at the producer-consumer boundary

The validator belongs at the boundary where a consumer is about to trust a fixed record. Validate before calculations, rendering assumptions, persistence decisions, or downstream transformations can obscure the original shape. Keep the record identifier alongside the decision, and preserve enough evidence to explain a rejection without serializing the source into a lossy form first.

A useful flow is straightforward: receive a candidate within the declared trust boundary; capture record identity; run validateRecord; on ACCEPT, hand the three values to code that assumes the fixed contract, without an intervening mutation; on rejection, retain status, failing index, and a descriptor/presence ledger appropriate to your privacy and logging policy. The consumer should not silently fill a hole or coerce an invalid slot merely to keep processing moving.

This boundary is narrower than transport correctness. The adjacent problem of stale fetch-response ownership deals with which asynchronous operation is authorized to update a UI; here we intentionally exclude network and async behavior and ask only whether a synchronous record is admissible. The shared engineering habit is to make the acceptance condition explicit at the boundary instead of inferring it from a convenient success signal.

If rejected records can be reprocessed, store source references or immutable upstream identifiers rather than only the lossy JSON array. That preserves the option to repair the producer and replay a corrected record. If only lossy evidence remains, hold reconstruction of the original record rather than inventing certainty about what its source object used to contain. The current candidate's rejection and the recovery workflow's HOLD can be recorded separately.

Repair the producer without inventing slot values

A validator can diagnose a missing required slot, but it cannot manufacture the missing domain fact. The corrective action belongs with the producer or with an authorized source-of-truth workflow. For a REJECT_MISSING_SLOT@1, the producer should regenerate or resubmit the record with a real value for index 1 derived from retained source information. For REJECT_VALUE@1, the producer should correct the invalid value under the domain rules. For REJECT_ACCESSOR@1, the producer should emit an own data property rather than a getter if this boundary contract is the intended one.

Do not fill holes with zero. Zero is explicitly outside the accepted range and the ZERO fixture exists to prove that. More importantly, even choosing an in-range default such as 1 would fabricate data. An arbitrary replacement can make the validator green while corrupting the business meaning of the record.

After producer repair, reprocess the candidate through the same admission gate and the same regression suite. Do not weaken the validator to accommodate malformed historical output unless the actual business contract has changed and that change is deliberate. A good regression fixture makes this cheap: the named rows become permanent examples of what must remain accepted or rejected.

Rollback limits should be explicit. If the only artifact retained from a malformed source is [2,null,6], no code can reconstruct whether that middle position was a hole, undefined, or null in the original JavaScript array. The correct action is not “best guess”; it is to obtain better source evidence or hold the record.

Choose accept, reject, repair, or hold

A production policy benefits from separating the immediate validation result from the operational next step:

Evidence state

Decision

Operational action

Array, length 3, all three own data slots, all values 1–9

ACCEPT

Continue with the record

Current object is malformed but reason is known

REJECT

Stop admission and record status/index

Authoritative source can regenerate a corrected value/shape

REPAIR

Fix producer or source, then reprocess

Only lossy serialized evidence survives, or trust boundary is outside declared scope

HOLD

Do not infer original shape; obtain better evidence

REJECT is the validator's immediate answer. REPAIR is a workflow decision after someone with authority over the producer or source confirms how to correct the record. HOLD is for uncertainty that the current evidence cannot resolve. These states prevent a common anti-pattern in shared libraries: quietly converting malformed inputs into accepted ones and making the caller unaware that information was invented or lost.

For the declared contract, only a record that passes all own-slot and value checks is accepted. Missing slots, accessors, wrong lengths, wrong types, and invalid values are rejected with a reason. The shared library should not “repair” on its own because it does not own the missing business fact.

The decision vocabulary also helps batch processing. A batch is not successful merely because a loop completed without throwing. Each record retains its ID and one of the four operational states. Accepted rows can advance; rejected rows remain visible as malformed; repairable rows can be linked to a source correction; held rows remain unresolved rather than disappearing into a retry queue. This is especially important for sparse inputs because “nothing happened at index 1” can otherwise look like “index 1 passed.” The decision ledger turns absence into an explicit operational fact.

Nor should aggregate counts replace record-level evidence. For example, ten accepted rows and one rejected row would not mean “ten out of eleven callbacks succeeded”: each record contains three required positions, and every can visit different populations. Preserve the record ID, status, failing index, and the slot ledger when diagnostic policy permits. Those facts explain the original admission decision. Reprocessing after a producer fix still requires an authoritative corrected candidate; a diagnostic ledger does not supply a missing business value.

Hold when the surviving evidence cannot reconstruct presence

Suppose a downstream log contains only the JSON string [2,null,6]. Under the serialization control above, that string is compatible with at least three earlier in-memory states: a hole, an own undefined, or an own null. The serialized artifact therefore cannot prove the producer's original slot presence.

A newly parsed [2,null,6] is a different question: its middle position is now an own data property holding null, so the gate rejects that current Array as REJECT_VALUE@1. That known current result does not recover the producer's earlier property shape. Keep the present validation decision separate from uncertainty about historical evidence.

That is a genuine evidence limit, not an invitation to choose the most likely case. Mark reconstruction of the record HOLD if the distinction matters to remediation and no better source remains. A descriptor/presence ledger captured before serialization would resolve that particular uncertainty because it preserves the fact that raw JSON erased. It would not, by itself, supply the missing integer. Evidence design belongs in the validator architecture, not only in later debugging.

Inputs outside the declared trust boundary also belong in hold-or-route logic. For example, if arbitrary Proxy objects are possible and the application requires hostile-input guarantees, this descriptor routine is not sufficient. Do not relabel an excluded threat model as “safe” because the ordinary-array fixture passed.

Assign ownership and maintain a regression contract

The producer owns construction of the three required own data slots and the correctness of their domain values. The shared validation library owns faithful enforcement of that declared contract and stable reason codes. The consumer owns the decision to admit, reject, route for repair, or hold a record, and it must not treat a Boolean from some unrelated iteration as a completeness certificate.

Keep the fixture factories in source control rather than flattening them into JSON snapshots. The holes, inherited prototype, deleted property, named property, and accessor are object-shape behaviors that a JSON round trip cannot faithfully preserve. A future refactor should rerun the factories and assert callback ledgers, own flags, gate statuses, failing indices, getter count, serialization control, and type controls.

The same resource-ownership mindset appears in DOM listener identity and cleanup, but the mechanisms differ: this test never touches the DOM. Here the ownership question is which object directly owns each required numeric property. Keeping the neighboring playbooks separate prevents a useful pattern from turning into a vague analogy.

The repeat run matters. The complete script below constructs fresh inputs for run-1 and run-2, then compares every returned ledger, including the slot evidence and controls. It also checks that the global Array and Object prototype descriptors remain unchanged. This supports deterministic reproduction for the executed runs. It is not a concurrency test, proof of all future executions, or a test of other JavaScript engines.

Finally, treat a harness failure differently from an expected record rejection. REJECT_MISSING_SLOT is a successful validator outcome for ONE_HOLE; an AssertionError means the implementation disagreed with the authored oracle and the regression test failed. Operational dashboards and CI output should preserve that distinction.

There is a practical review benefit to keeping the naive diagnostic in the suite even after the repaired gate ships. It documents the original failure mode and prevents future maintainers from “simplifying” the validator back to array.every(predicate). The diagnostic is not production admission code; it is executable explanatory evidence. When a language primitive has semantics that are correct but weaker than the business contract, retaining the counterexample beside the repair is often more durable than relying on a comment that says “do not change this.”

The same principle applies to the accessor and JSON controls. They are not edge-case decoration. The accessor row protects the no-read-before-kind-check boundary, while the equal JSON outputs protect the evidence-retention rule. Together with the main rows, they define what a future refactor must continue to distinguish even if the implementation changes completely.

Save the complete CommonJS regression file below as sparse-slot-validation.cjs. It includes the literal oracle, reason-code assertions, accessor no-read control, pre-serialization evidence, type controls, and a second run with newly constructed inputs. The version assertion deliberately fails when the declared Node.js baseline is not used. That is a harness precondition failure, not a malformed-record status.

'use strict';

const assert = require('node:assert/strict');
assert.equal(process.version, 'v22.16.0', 'Use the declared Node.js baseline');

const globalArrayBefore = Object.getOwnPropertyDescriptors(Array.prototype);
const globalObjectBefore = Object.getOwnPropertyDescriptors(Object.prototype);
for (const key of ['0', '1', '2']) {
  assert.equal(key in Array.prototype, false, 'Use clean global prototypes');
}

const STATUS = Object.freeze({
  ACCEPT: 'ACCEPT',
  REJECT_TYPE: 'REJECT_TYPE',
  REJECT_LENGTH: 'REJECT_LENGTH',
  REJECT_MISSING_SLOT: 'REJECT_MISSING_SLOT',
  REJECT_ACCESSOR: 'REJECT_ACCESSOR',
  REJECT_VALUE: 'REJECT_VALUE',
});

function isBusinessValue(v) {
  return Number.isInteger(v) && v >= 1 && v <= 9;
}

// Production interface: the caller must enforce the non-Proxy trust boundary.
function validateRecord(input) {
  if (!Array.isArray(input)) {
    return { status: STATUS.REJECT_TYPE, index: null };
  }
  if (input.length !== 3) {
    return { status: STATUS.REJECT_LENGTH, index: null };
  }
  for (let i = 0; i < 3; i += 1) {
    const descriptor = Object.getOwnPropertyDescriptor(input, String(i));
    if (descriptor === undefined) {
      return { status: STATUS.REJECT_MISSING_SLOT, index: i };
    }
    if (!Object.hasOwn(descriptor, 'value')) {
      return { status: STATUS.REJECT_ACCESSOR, index: i };
    }
    if (!isBusinessValue(descriptor.value)) {
      return { status: STATUS.REJECT_VALUE, index: i };
    }
  }
  return { status: STATUS.ACCEPT, index: null };
}

function makeInherited() {
  const array = [2, , 6];
  const localPrototype = Object.create(Array.prototype);
  Object.defineProperty(localPrototype, '1', { value: 4 });
  Object.setPrototypeOf(array, localPrototype);
  return array;
}

function makeDeleted() {
  const array = [2, 4, 6];
  delete array[1];
  return array;
}

function makeNamed() {
  const array = [2, , 6];
  array.extra = 4;
  return array;
}

function makeAccessor(counter) {
  const array = [2, 4, 6];
  Object.defineProperty(array, '1', {
    configurable: true,
    enumerable: true,
    get() {
      counter.count += 1;
      return 4;
    },
  });
  return array;
}

const MAIN_FACTORIES = Object.freeze({
  DENSE: () => [2, 4, 6],
  ALL_HOLES: () => new Array(3),
  ONE_HOLE: () => [2, , 6],
  UNDEFINED: () => [2, undefined, 6],
  EMPTY: () => [],
  ZERO: () => [2, 0, 6],
  INHERITED: makeInherited,
  DELETED: makeDeleted,
  NAMED: makeNamed,
});

// Literal oracle: never derive these decisions from validateRecord.
const EXPECTED_MAIN = Object.freeze([
  {
    id: 'DENSE', every: true, visited: [0, 1, 2], length: 3,
    own: [true, true, true],
    gate: { status: STATUS.ACCEPT, index: null },
  },
  {
    id: 'ALL_HOLES', every: true, visited: [], length: 3,
    own: [false, false, false],
    gate: { status: STATUS.REJECT_MISSING_SLOT, index: 0 },
  },
  {
    id: 'ONE_HOLE', every: true, visited: [0, 2], length: 3,
    own: [true, false, true],
    gate: { status: STATUS.REJECT_MISSING_SLOT, index: 1 },
  },
  {
    id: 'UNDEFINED', every: false, visited: [0, 1], length: 3,
    own: [true, true, true],
    gate: { status: STATUS.REJECT_VALUE, index: 1 },
  },
  {
    id: 'EMPTY', every: true, visited: [], length: 0,
    own: [false, false, false],
    gate: { status: STATUS.REJECT_LENGTH, index: null },
  },
  {
    id: 'ZERO', every: false, visited: [0, 1], length: 3,
    own: [true, true, true],
    gate: { status: STATUS.REJECT_VALUE, index: 1 },
  },
  {
    id: 'INHERITED', every: true, visited: [0, 1, 2], length: 3,
    own: [true, false, true],
    gate: { status: STATUS.REJECT_MISSING_SLOT, index: 1 },
  },
  {
    id: 'DELETED', every: true, visited: [0, 2], length: 3,
    own: [true, false, true],
    gate: { status: STATUS.REJECT_MISSING_SLOT, index: 1 },
  },
  {
    id: 'NAMED', every: true, visited: [0, 2], length: 3,
    own: [true, false, true],
    gate: { status: STATUS.REJECT_MISSING_SLOT, index: 1 },
  },
]);

function naiveDiagnostic(array) {
  const visited = [];
  const every = Array.prototype.every.call(array, (value, index) => {
    visited.push(index);
    return isBusinessValue(value);
  });
  return {
    every,
    visited,
    length: array.length,
    own: [0, 1, 2].map((i) => Object.hasOwn(array, String(i))),
  };
}

// Diagnostic ledger, not a universal object serializer or Proxy sandbox.
function classifyRequiredSlots(array) {
  return [0, 1, 2].map((i) => {
    const key = String(i);
    const descriptor = Object.getOwnPropertyDescriptor(array, key);
    if (descriptor === undefined) {
      return {
        index: i,
        presence: key in array ? 'inherited' : 'absent',
        own: false, kind: null, valueKind: 'unread', value: null,
      };
    }
    if (!Object.hasOwn(descriptor, 'value')) {
      return {
        index: i, presence: 'own', own: true,
        kind: 'accessor', valueKind: 'unread', value: null,
      };
    }
    const v = descriptor.value;
    return {
      index: i, presence: 'own', own: true, kind: 'data',
      valueKind: v === null ? 'null' : typeof v,
      value: typeof v === 'number' && Number.isFinite(v) ? v : null,
    };
  });
}

function runSuite(label) {
  assert.deepEqual(
    Object.keys(MAIN_FACTORIES),
    EXPECTED_MAIN.map((row) => row.id),
    ${label}: preserve the full ordered fixture population,
  );
  const rows = [];
  for (const expected of EXPECTED_MAIN) {
    const array = MAIN_FACTORIES[expected.id]();
    const slots = classifyRequiredSlots(array);
    const naive = naiveDiagnostic(array);
    const gate = validateRecord(array);
    const actual = { id: expected.id, ...naive, gate };
    assert.deepEqual(actual, expected, ${label} ${expected.id}: oracle);
    assert.deepEqual(slots.map((slot) => slot.index), [0, 1, 2]);
    assert.deepEqual(slots.map((slot) => slot.own), expected.own);
    if (expected.id === 'DELETED') assert.equal(array.length, 3);
    if (expected.id === 'NAMED') {
      assert.equal(Object.keys(array).length, 3);
      assert.deepEqual(Object.keys(array), ['0', '2', 'extra']);
      assert.equal(Object.hasOwn(array, '1'), false);
    }
    rows.push({ ...actual, slots });
  }
  assert.deepEqual(
    rows.filter((row) => row.gate.status === STATUS.ACCEPT).map((row) => row.id),
    ['DENSE'],
  );

  const counter = { count: 0 };
  const accessor = makeAccessor(counter);
  const accessorGate = validateRecord(accessor);
  assert.deepEqual(accessorGate, { status: STATUS.REJECT_ACCESSOR, index: 1 });
  assert.equal(counter.count, 0, ${label}: admission must not read the getter);

  const nullGate = validateRecord(null);
  assert.deepEqual(nullGate, { status: STATUS.REJECT_TYPE, index: null });
  const arrayLikeGate = validateRecord({ 0: 2, 1: 4, 2: 6, length: 3 });
  assert.deepEqual(arrayLikeGate, { status: STATUS.REJECT_TYPE, index: null });

  // Preserve each source's ledger before serializing that same source array.
  const jsonSources = [
    { id: 'JSON_HOLE', array: [2, , 6] },
    { id: 'JSON_UNDEFINED', array: [2, undefined, 6] },
    { id: 'JSON_NULL', array: [2, null, 6] },
  ];
  const evidence = jsonSources.map(({ id, array }) => ({
    id, slots: classifyRequiredSlots(array),
  }));
  evidence.push(
    { id: 'INHERITED', slots: classifyRequiredSlots(makeInherited()) },
    { id: 'ACCESSOR', slots: classifyRequiredSlots(accessor) },
  );
  assert.deepEqual(evidence.map((row) => row.slots[1]), [
    {
      index: 1, presence: 'absent', own: false,
      kind: null, valueKind: 'unread', value: null,
    },
    {
      index: 1, presence: 'own', own: true,
      kind: 'data', valueKind: 'undefined', value: null,
    },
    {
      index: 1, presence: 'own', own: true,
      kind: 'data', valueKind: 'null', value: null,
    },
    {
      index: 1, presence: 'inherited', own: false,
      kind: null, valueKind: 'unread', value: null,
    },
    {
      index: 1, presence: 'own', own: true,
      kind: 'accessor', valueKind: 'unread', value: null,
    },
  ]);
  assert.equal(counter.count, 0, ${label}: evidence must not read the getter);
  const json = jsonSources.map(({ id, array }) => ({
    id, text: JSON.stringify(array),
  }));
  assert.deepEqual(json, [
    { id: 'JSON_HOLE', text: '[2,null,6]' },
    { id: 'JSON_UNDEFINED', text: '[2,null,6]' },
    { id: 'JSON_NULL', text: '[2,null,6]' },
  ]);
  assert.deepEqual(JSON.parse(JSON.stringify(evidence)), evidence);

  const spreadHole = [...[2, , 6]];
  const fromHole = Array.from([2, , 6]);
  for (const array of [spreadHole, fromHole]) {
    assert.equal(Object.hasOwn(array, '1'), true);
    assert.equal(array[1], undefined);
    assert.deepEqual(validateRecord(array), {
      status: STATUS.REJECT_VALUE, index: 1,
    });
  }
  return {
    rows,
    controls: {
      ACCESSOR: { gate: accessorGate, getterCalls: counter.count },
      NULL: { gate: nullGate },
      ARRAY_LIKE_OBJECT: { gate: arrayLikeGate },
    },
    json, evidence, namedKeys: Object.keys(MAIN_FACTORIES.NAMED()),
  };
}

const run1 = runSuite('run-1');
const run2 = runSuite('run-2');
assert.deepEqual(run2, run1, 'Fresh inputs must reproduce every returned ledger');
assert.deepEqual(
  Object.getOwnPropertyDescriptors(Array.prototype), globalArrayBefore,
  'Array.prototype must remain unchanged',
);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(Object.prototype), globalObjectBefore,
  'Object.prototype must remain unchanged',
);

console.logNode ${process.version}; V8 ${process.versions.v8});
for (const [label, run] of [['run-1', run1], ['run-2', run2]]) {
  console.log\n${label});
  for (const row of run.rows) {
    console.log(
      ${row.id.padEnd(10)} every=${String(row.every).padEnd(5)}  +
      visited=[${row.visited.join(',')}] length=${row.length}  +
      own=[${row.own.join(',')}] gate=${row.gate.status} +
      ${row.gate.index === null ? '' : @${row.gate.index},
    );
  }
  console.log(
    ACCESSOR   gate=${run.controls.ACCESSOR.gate.status}@1  +
    getterCalls=${run.controls.ACCESSOR.getterCalls},
  );
  console.logNULL       gate=${run.controls.NULL.gate.status});
  console.logARRAY_LIKE gate=${run.controls.ARRAY_LIKE_OBJECT.gate.status});
  console.logJSON       ${run.json.map((row) => row.text).join(' === ')});
  console.logNAMED keys [${run.namedKeys.join(',')}] count=${run.namedKeys.length});
  console.log('EVIDENCE   slot ledgers preserved through JSON round trip');
}
console.log('\nAll assertions passed.');
Observed output from this complete file under Node.js v22.16.0 and V8 12.4.254.21-node.26 on Linux x64:
Node v22.16.0; V8 12.4.254.21-node.26

run-1
DENSE      every=true  visited=[0,1,2] length=3 own=[true,true,true] gate=ACCEPT
ALL_HOLES  every=true  visited=[] length=3 own=[false,false,false] gate=REJECT_MISSING_SLOT@0
ONE_HOLE   every=true  visited=[0,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
UNDEFINED  every=false visited=[0,1] length=3 own=[true,true,true] gate=REJECT_VALUE@1
EMPTY      every=true  visited=[] length=0 own=[false,false,false] gate=REJECT_LENGTH
ZERO       every=false visited=[0,1] length=3 own=[true,true,true] gate=REJECT_VALUE@1
INHERITED  every=true  visited=[0,1,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
DELETED    every=true  visited=[0,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
NAMED      every=true  visited=[0,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
ACCESSOR   gate=REJECT_ACCESSOR@1 getterCalls=0
NULL       gate=REJECT_TYPE
ARRAY_LIKE gate=REJECT_TYPE
JSON       [2,null,6] === [2,null,6] === [2,null,6]
NAMED keys [0,2,extra] count=3
EVIDENCE   slot ledgers preserved through JSON round trip

run-2
DENSE      every=true  visited=[0,1,2] length=3 own=[true,true,true] gate=ACCEPT
ALL_HOLES  every=true  visited=[] length=3 own=[false,false,false] gate=REJECT_MISSING_SLOT@0
ONE_HOLE   every=true  visited=[0,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
UNDEFINED  every=false visited=[0,1] length=3 own=[true,true,true] gate=REJECT_VALUE@1
EMPTY      every=true  visited=[] length=0 own=[false,false,false] gate=REJECT_LENGTH
ZERO       every=false visited=[0,1] length=3 own=[true,true,true] gate=REJECT_VALUE@1
INHERITED  every=true  visited=[0,1,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
DELETED    every=true  visited=[0,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
NAMED      every=true  visited=[0,2] length=3 own=[true,false,true] gate=REJECT_MISSING_SLOT@1
ACCESSOR   gate=REJECT_ACCESSOR@1 getterCalls=0
NULL       gate=REJECT_TYPE
ARRAY_LIKE gate=REJECT_TYPE
JSON       [2,null,6] === [2,null,6] === [2,null,6]
NAMED keys [0,2,extra] count=3
EVIDENCE   slot ledgers preserved through JSON round trip

All assertions passed.

Build reliable JavaScript habits through guided practice

The durable lesson is narrower than “avoid every.” every is correct for its own contract. The mistake is using its true result as proof that every required own position in a fixed record existed and was validated. Sparse arrays, inherited numeric properties, early termination, and lossy serialization each expose a different way that callback success can diverge from record completeness. The robust response is to define the business population explicitly, inspect each required own descriptor, reject accessors before value reads, and preserve evidence before transformations can erase it.

For maintainers expanding their broader frontend development learning path, this exercise shows why language-level contracts deserve the same attention as frameworks and UI architecture. Readers building stronger JavaScript and frontend foundations may review the FrontEnd Developer Program's published curriculum and workload. Its Program Specifics section states four months and 10–12 hours per week. The page lists JavaScript ES6+, HTML/CSS, React, state management, API interaction, educational mentorship, and projects. It states that successful completion earns a Training Certificate and a Certificate of Internship. Those published details do not establish that this exact sparse-array laboratory is included or guarantee an external job.