denoland/deno · error · DOMException

InvalidStateError

InvalidStateError

Error message

Invalid FileReader state

What it means

InvalidStateError DOMException thrown at the top of FileReader's #readOperation (ext/web/10_filereader.js), implementing the spec's step 1: if the FileReader's state is already "loading" — a read is in flight — starting another read is rejected. Each FileReader performs one read at a time; readiness is observable via the readyState property (EMPTY=0, LOADING=1, DONE=2).

Source

Thrown at ext/web/10_filereader.js:76

class FileReader extends EventTarget {
  /** @type {"empty" | "loading" | "done"} */
  [state] = "empty";
  /** @type {null | string | ArrayBuffer} */
  [result] = null;
  /** @type {null | DOMException} */
  [error] = null;
  /** @type {null | {aborted: boolean}} */
  [aborted] = null;

  /**
   * @param {Blob} blob
   * @param {{kind: "ArrayBuffer" | "Text" | "DataUrl" | "BinaryString", encoding?: string}} readtype
   */
  #readOperation(blob, readtype) {
    // 1. If fr's state is "loading", throw an InvalidStateError DOMException.
    if (this[state] === "loading") {
      throw new DOMException(
        "Invalid FileReader state",
        "InvalidStateError",
      );
    }
    // 2. Set fr's state to "loading".
    this[state] = "loading";
    // 3. Set fr's result to null.
    this[result] = null;
    // 4. Set fr's error to null.
    this[error] = null;

    // We set this[aborted] to a new object, and keep track of it in a
    // separate variable, so if a new read operation starts while there are
    // remaining tasks from a previous aborted operation, the new operation
    // will run while the tasks from the previous one are still aborted.
    const abortedState = this[aborted] = { aborted: false };

    // 5. Let stream be the result of calling get stream on blob.

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Await completion before the next read: listen for loadend (or wrap the read in a Promise resolving on loadend/onerror/onabort) and chain reads sequentially.
  2. Use a new FileReader per read — they are cheap and stateless between reads.
  3. Prefer the promise APIs when possible: await blob.text(), blob.arrayBuffer(), or blob.data URL equivalents instead of FileReader.
  4. Guard with if (reader.readyState !== FileReader.LOADING) before calling readAs*.

Example fix

// before
for (const f of files) reader.readAsText(f); // 2nd call throws: still loading

// after
function read(file) {
  return new Promise((res, rej) => {
    reader.onload = () => res(reader.result);
    reader.onerror = () => rej(reader.error);
    reader.readAsText(file);
  });
}
for (const f of files) await read(f); // sequential, always EMPTY when started
Defensive patterns

Strategy: validation

Validate before calling

// readyState: EMPTY(0) LOADING(1) DONE(2) — only start when EMPTY/DONE.
function readText(reader, blob) {
  if (reader.readyState === FileReader.LOADING) {
    return Promise.reject(new Error('FileReader busy'));
  }
  return new Promise((res, rej) => {
    reader.onload = () => res(reader.result);
    reader.onerror = () => rej(reader.error);
    reader.readAsText(blob);
  });
}

Type guard

function fileReaderIdle(reader) {
  return reader.readyState !== FileReader.LOADING;
}

Try / catch

try {
  reader.readAsText(blob);
} catch (err) {
  if (err instanceof DOMException && err.name === 'InvalidStateError') {
    return queueReadAfterLoadend(reader, blob); // retry once idle
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling readAsText/readAsArrayBuffer/readAsDataURL/readAsBinaryString while a previous readAs* on the same FileReader has not fired loadend yet — e.g. issuing reads in a loop without awaiting completion, or firing a second read from a click handler while the first is still running.

Common situations: Loops that read a list of files with one shared FileReader; UI handlers that re-trigger on double-click before the first read finishes; code migrated from callback style where each read assumed a fresh reader.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/76ade52ef7eae869. Report an issue: GitHub.