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
- Make the read callback explicitly return a Buffer for every path the resolver queries, or throw a clear ENOENT-style error you can recognize.
- Log the path and any thrown error inside the callback before re-throwing, since the wrapping hides it.
- Verify the callback return type matches what napi expects (a JsBuffer/Buffer, not a string or TypedArray).
- 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
- Always return a Buffer from the read callback; never undefined or a string.
- Log every path queried by read in development to spot unhandled branches.
- Throw a recognizable ENOENT error for genuinely missing paths so the NotFound mapping is semantically correct.
- Keep the callback pure and synchronous relative to the napi thread — long async work breaks the contract.
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
- GenericFailure
- GenericFailure
- Resolvers must return an absolute path, ${resolver.name} ret
- pnpm failed to install modules
- The import path: ${dependency.specifier} is using webpack sp
AI-assisted analysis of parcel-bundler/parcel@59484858a1 (2026-08-13).
Data as JSON: /api/errors/d8cf6dedcb122461.
Report an issue: GitHub.