parcel-bundler/parcel · error · std::io::Error

NotFound

NotFound

Error message

[dynamic error from JS read callback: ${err}]

What it means

Thrown by JsFileSystem::read_to_string when the user-supplied JavaScript read callback fails for any reason. The napi error from the callback (wrong return type, thrown exception, failed buffer conversion) is stringified and wrapped as std::io::ErrorKind::NotFound. This is intentionally lossy: the resolver treats unreadable paths as missing, so any callback defect surfaces to the caller as a NotFound resolution failure rather than the real cause.

Source

Thrown at crates/node-bindings/src/resolver.rs:89

}

pub struct JsFileSystem {
  pub read: FunctionRef,
  pub kind: FunctionRef,
  pub read_link: FunctionRef,
}

impl FileSystem for JsFileSystem {
  fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
    let read = || -> napi::Result<_> {
      let path = path.to_string_lossy();
      let path = self.read.env.create_string(path.as_ref())?;
      let res: JsBuffer = self.read.get()?.call(None, &[path])?.try_into()?;
      let value = res.into_value()?;
      Ok(unsafe { String::from_utf8_unchecked(value.to_vec()) })
    };

    read().map_err(|err| std::io::Error::new(std::io::ErrorKind::NotFound, err.to_string()))
  }

  fn kind(&self, path: &Path) -> FileKind {
    let kind = || -> napi::Result<u32> {
      let path = path.to_string_lossy();
      let p = self.kind.env.create_string(path.as_ref())?;
      let res: JsNumber = self.kind.get()?.call(None, &[p])?.try_into()?;
      res.get_uint32()
    };

    match kind() {
      Ok(num) => FileKind::from_bits_truncate(num as u8),
      _ => FileKind::empty(),
    }
  }

  fn read_link(&self, path: &Path) -> std::io::Result<PathBuf> {
    let canonicalize = || -> napi::Result<_> {

View on GitHub (pinned to 59484858a1)

Solutions

  1. Make the read callback explicitly return a Buffer for every path the resolver queries, or throw a clear ENOENT-style error you can recognize.
  2. Log the path and any thrown error inside the callback before re-throwing, since the wrapping hides it.
  3. Verify the callback return type matches what napi expects (a JsBuffer/Buffer, not a string or TypedArray).
  4. Test the callback in isolation against the exact path strings the resolver generates (absolute, normalized, with platform separators).

Example fix

// before
const fs = {
  read(path) {
    return files[path]; // undefined if missing → napi conversion throws → NotFound
  },
  read_link, kind,
};

// after — always return a Buffer, log on miss
const fs = {
  read(path) {
    const entry = files[path];
    if (entry == null) {
      console.warn('[fs.read] miss:', path);
      throw new Error('ENOENT: ' + path);
    }
    return Buffer.from(entry, 'utf8');
  },
  read_link, kind,
};
Defensive patterns

Strategy: validation

Validate before calling

// Validate the read callback contract before constructing the Resolver
function checkReadCallback(read) {
  const probe = read('__probe__');
  if (probe !== undefined && !(probe instanceof Uint8Array) && !Buffer.isBuffer(probe)) {
    // a throw is fine (path legitimately missing); a non-Buffer non-throw is a bug
    throw new Error('fs.read must return a Buffer or throw, got: ' + typeof probe);
  }
}
checkReadCallback(read);

Type guard

function isValidFsRead(read) {
  return typeof read === 'function';
}
// stronger runtime contract: wrap to enforce return type
function safeRead(read) {
  return (path) => {
    const result = read(path);
    if (result === undefined) throw new Error('ENOENT: ' + path);
    if (!Buffer.isBuffer(result) && !(result instanceof Uint8Array)) {
      throw new TypeError('fs.read must return a Buffer for: ' + path);
    }
    return result;
  };
}

Try / catch

try {
  return resolver.resolve(opts);
} catch (e) {
  if (e.message.includes('NotFound') || e.code === 'NotFound') {
    // likely a fs.read callback defect; re-run read in isolation to surface real cause
    throw new Error(`Resolution failed (possibly fs.read defect). Probe path: ${opts.filename}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Constructing a Resolver with a custom fs where the read function throws, returns undefined/a string instead of a Buffer/ArrayBuffer, or the path is absent from the virtual filesystem. The stringified callback error replaces the real io::ErrorKind, so permission errors, encoding errors, and logic bugs all look identical.

Common situations: Implementing an in-memory or remote filesystem for the resolver; the read callback has an unhandled branch for a path the resolver probes (e.g. package.json in a directory that does not exist); the callback returns a Node Buffer in a wasm build where JsBuffer expectations differ; a typo in the callback referencing an undefined variable.

Related errors


AI-assisted analysis of parcel-bundler/parcel@59484858a1 (2026-08-13). Data as JSON: /api/errors/d8cf6dedcb122461. Report an issue: GitHub.