{"record":{"id":"84bc986dcc433010","repo":"krisk/Fuse","slug":"invalid-doc-index-must-be-a-non-negative-integer","errorCode":null,"errorMessage":"Invalid doc index: must be a non-negative integer within the bounds of the docs array","messagePattern":"Invalid doc index: must be a non-negative integer within the bounds of the docs array","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/core/index.ts","lineNumber":198,"sourceCode":"      }\n\n      // Filter docs in a single pass instead of reverse-splicing\n      const toRemove = new Set(indicesToRemove)\n      this._docs = this._docs.filter((_, i) => !toRemove.has(i))\n      this._myIndex.removeAll(indicesToRemove)\n\n      this._invalidateSearcherCache()\n    }\n\n    return results\n  }\n\n  removeAt(idx: number): T {\n    // Validate before any mutation. The previous code spliced `_docs` first\n    // and let FuseIndex.removeAt throw afterward — partial-state on invalid\n    // input. Atomic now.\n    if (!Number.isInteger(idx) || idx < 0 || idx >= this._docs.length) {\n      throw new Error(ErrorMsg.INVALID_DOC_INDEX)\n    }\n\n    if (this._invertedIndex) {\n      removeAndShiftInvertedIndex(this._invertedIndex, [idx])\n    }\n    const doc = this._docs.splice(idx, 1)[0]\n    this._myIndex.removeAt(idx)\n    this._invalidateSearcherCache()\n    return doc\n  }\n\n  _invalidateSearcherCache(): void {\n    this._lastQuery = null\n    this._lastSearcher = null\n  }\n\n  getIndex(): FuseIndex<T> {\n    return this._myIndex","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/krisk/Fuse/blob/edf2fb608eca0461508d1d71317e6e58309ffada/src/core/index.ts#L180-L216","documentation":"Fuse.removeAt(idx) validates its argument before any mutation: idx must be an integer, non-negative, and within bounds of the docs array. The check is atomic — an invalid call leaves the collection and inverted index completely untouched (the old implementation spliced first and threw after, leaving partial state).","triggerScenarios":"fuse.removeAt(-1), a float like 1.5, NaN/undefined from an unbound handler, or an index >= docs.length — e.g. reusing a stale index captured before prior removals shrank the collection.","commonSituations":"Removing items in a loop without adjusting indices; deleting based on a cached position; passing an id or event object instead of the numeric position.","solutions":["Validate before calling: ensure Number.isInteger(idx) && idx >= 0 && idx < docs.length.","Prefer remove(doc) or remove(predicate) when you have the document object instead of positional removal.","Recompute the index immediately before removal rather than caching it; iterate backwards when removing multiple items."],"exampleFix":"// before\nlist.forEach((_, i) => fuse.removeAt(i)) // out of bounds as list shrinks\n// after\nfor (let i = list.length - 1; i >= 0; i--) fuse.removeAt(i)","handlingStrategy":"validation","validationCode":"const canRemoveAt = (docsLen, idx) =>\n  Number.isInteger(idx) && idx >= 0 && idx < docsLen\nif (canRemoveAt(docs.length, idx)) {\n  fuse.removeAt(idx)\n}","typeGuard":"const isValidDocIndex = (idx, len) =>\n  Number.isInteger(idx) && idx >= 0 && idx < len","tryCatchPattern":"try {\n  fuse.removeAt(idx)\n} catch (e) {\n  if (String(e.message).startsWith('Invalid doc index')) {\n    return // stale index / already removed; collection untouched\n  }\n  throw e\n}","preventionTips":["Recompute indices immediately before removal; never cache them across mutations.","Iterate backwards when removing multiple items by index.","Prefer remove(predicate) over positional removal when you hold the document."],"tags":["argument-validation","bounds-check","off-by-one"],"backgroundTag":"index-out-of-range","analyzedSha":"edf2fb608eca0461508d1d71317e6e58309ffada","analyzedAt":"2026-09-02T02:46:54.623Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T06:17:21.866Z"}