tailwindlabs/tailwindcss · critical
Failed to read file
Error message
Failed to read file
What it means
Panics (via .expect) in the napi binding get_candidates_with_positions when ChangedContent.file is set but std::fs::read_to_string fails to read it (file missing, unreadable, or a permission error). This is the Node-facing wrapper around the Rust scanner; when content is None it falls back to reading the file path and unwinds on any IO error.
Source
Thrown at crates/node/src/lib.rs:141
#[napi]
pub fn scan(&mut self) -> Vec<String> {
self.scanner.scan()
}
#[napi]
pub fn scan_files(&mut self, input: Vec<ChangedContent>) -> Vec<String> {
self
.scanner
.scan_content(input.into_iter().map(Into::into).collect())
}
#[napi]
pub fn get_candidates_with_positions(
&mut self,
input: ChangedContent,
) -> Vec<CandidateWithPosition> {
let content = input.content.unwrap_or_else(|| {
std::fs::read_to_string(input.file.unwrap()).expect("Failed to read file")
});
let input = ChangedContent {
file: None,
content: Some(content.clone()),
extension: input.extension,
};
let mut utf16_idx = IndexConverter::new(&content[..]);
self
.scanner
.get_candidates_with_positions(input.into())
.into_iter()
.map(|(candidate, position)| CandidateWithPosition {
candidate,
position: utf16_idx.get(position),
})View on GitHub (pinned to 16e94cbf7f)
Solutions
- Ensure the file path exists and is readable before calling: use fs.existsSync / fs.accessSync.
- Pass content directly in ChangedContent.content (with extension set) instead of a file path, reading it on the JS side with error handling.
- Validate paths and filter out missing files before submitting to the native scanner.
Example fix
// before — panics if file is gone
scanner.getCandidatesWithPositions({ file: path })
// after
const content = fs.readFileSync(path, 'utf8')
scanner.getCandidatesWithPositions({ content, extension: 'html' }) Defensive patterns
Strategy: validation
Validate before calling
// JS side: verify file exists and is readable before the native call
const fs = require('fs')
if (input.file && (!fs.existsSync(input.file) || !fs.statSync(input.file).isFile())) {
throw new Error(`Cannot read candidate source: ${input.file}`)
}
scanner.getCandidatesWithPositions(input) Type guard
function isReadableFile(file: string): boolean {
try {
return fs.statSync(file).isFile()
} catch {
return false
}
} Try / catch
// Prefer passing content to avoid the native panic
let content
try {
content = fs.readFileSync(input.file, 'utf8')
} catch (e) {
console.warn(`skipping unreadable file: ${input.file}`)
return []
}
scanner.getCandidatesWithPositions({ content, extension: input.extension }) Prevention
- Prefer passing content (read with error handling on the JS side) over file paths.
- Validate file existence and readability before handing paths to the native scanner.
- Filter deleted/unreadable files from the candidate list up front.
When it happens
Trigger: Calling the native get_candidates_with_positions with a ChangedContent whose file path does not exist, is not readable, or is a directory. The .expect() turns the IO error into a panic that crosses the napi boundary.
Common situations: Passing a stale or deleted file path to the scanner. Path resolution mismatch (relative vs absolute) between the JS caller and the working directory. Permission errors in restricted environments. Race where the file is deleted between scan scheduling and read.
Related errors
- <stdin> has no metadata
- Input must be valid UTF-8
- The browser build does not support @import for "${id}"
- The `source(${compiler.root.pattern})` does not exist or is
- The path given to `source(…)` must be a directory but got `s
AI-assisted analysis of tailwindlabs/tailwindcss@16e94cbf7f (2026-08-12).
Data as JSON: /api/errors/ad292345c10b8a97.
Report an issue: GitHub.