{"record":{"id":"d8cf6dedcb122461","repo":"parcel-bundler/parcel","slug":"notfound","errorCode":"NotFound","errorMessage":"[dynamic error from JS read callback: ${err}]","messagePattern":"\\[dynamic error from JS read callback: (.+?)\\]","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/node-bindings/src/resolver.rs","lineNumber":89,"sourceCode":"}\n\npub struct JsFileSystem {\n  pub read: FunctionRef,\n  pub kind: FunctionRef,\n  pub read_link: FunctionRef,\n}\n\nimpl FileSystem for JsFileSystem {\n  fn read_to_string(&self, path: &Path) -> std::io::Result<String> {\n    let read = || -> napi::Result<_> {\n      let path = path.to_string_lossy();\n      let path = self.read.env.create_string(path.as_ref())?;\n      let res: JsBuffer = self.read.get()?.call(None, &[path])?.try_into()?;\n      let value = res.into_value()?;\n      Ok(unsafe { String::from_utf8_unchecked(value.to_vec()) })\n    };\n\n    read().map_err(|err| std::io::Error::new(std::io::ErrorKind::NotFound, err.to_string()))\n  }\n\n  fn kind(&self, path: &Path) -> FileKind {\n    let kind = || -> napi::Result<u32> {\n      let path = path.to_string_lossy();\n      let p = self.kind.env.create_string(path.as_ref())?;\n      let res: JsNumber = self.kind.get()?.call(None, &[p])?.try_into()?;\n      res.get_uint32()\n    };\n\n    match kind() {\n      Ok(num) => FileKind::from_bits_truncate(num as u8),\n      _ => FileKind::empty(),\n    }\n  }\n\n  fn read_link(&self, path: &Path) -> std::io::Result<PathBuf> {\n    let canonicalize = || -> napi::Result<_> {","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/parcel-bundler/parcel/blob/59484858a1a0bcbb71f74088956bb437a2db6505/crates/node-bindings/src/resolver.rs#L71-L107","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nconst fs = {\n  read(path) {\n    return files[path]; // undefined if missing → napi conversion throws → NotFound\n  },\n  read_link, kind,\n};\n\n// after — always return a Buffer, log on miss\nconst fs = {\n  read(path) {\n    const entry = files[path];\n    if (entry == null) {\n      console.warn('[fs.read] miss:', path);\n      throw new Error('ENOENT: ' + path);\n    }\n    return Buffer.from(entry, 'utf8');\n  },\n  read_link, kind,\n};","handlingStrategy":"validation","validationCode":"// Validate the read callback contract before constructing the Resolver\nfunction checkReadCallback(read) {\n  const probe = read('__probe__');\n  if (probe !== undefined && !(probe instanceof Uint8Array) && !Buffer.isBuffer(probe)) {\n    // a throw is fine (path legitimately missing); a non-Buffer non-throw is a bug\n    throw new Error('fs.read must return a Buffer or throw, got: ' + typeof probe);\n  }\n}\ncheckReadCallback(read);","typeGuard":"function isValidFsRead(read) {\n  return typeof read === 'function';\n}\n// stronger runtime contract: wrap to enforce return type\nfunction safeRead(read) {\n  return (path) => {\n    const result = read(path);\n    if (result === undefined) throw new Error('ENOENT: ' + path);\n    if (!Buffer.isBuffer(result) && !(result instanceof Uint8Array)) {\n      throw new TypeError('fs.read must return a Buffer for: ' + path);\n    }\n    return result;\n  };\n}","tryCatchPattern":"try {\n  return resolver.resolve(opts);\n} catch (e) {\n  if (e.message.includes('NotFound') || e.code === 'NotFound') {\n    // likely a fs.read callback defect; re-run read in isolation to surface real cause\n    throw new Error(`Resolution failed (possibly fs.read defect). Probe path: ${opts.filename}`);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["resolver","virtual-filesystem","napi","callback","error-masking"],"backgroundTag":null,"analyzedSha":"59484858a1a0bcbb71f74088956bb437a2db6505","analyzedAt":"2026-08-13T04:06:35.925Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}