facebook/relay · error
LocalPersister: Unable to read the {} file: {}
Error message
LocalPersister: Unable to read the {} file: {} What it means
LocalPersister::new reads the persisted-operations text file on startup. A NotFound error is treated as a first run (empty map), but any other read/parse-adjacent IO error (permissions, is-a-directory, IO failure) panics because the compiler cannot safely manage persisted operations without reading the existing state.
Source
Thrown at compiler/crates/relay-compiler/src/operation_persister/local_persister.rs:47
/// This struct implements the `OperationPersister` trait, which defines the interface for persisting GraphQL operations.
pub struct LocalPersister {
/// The configuration for the local persister.
config: LocalPersistConfig,
/// A map of query IDs to query texts.
query_map: DashMap<String, String>,
}
impl LocalPersister {
pub fn new(config: LocalPersistConfig) -> Self {
let query_map: DashMap<String, String> = match std::fs::read_to_string(&config.file) {
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
Err(e) if e.kind() == ErrorKind::NotFound => {
// First run: the file doesn't exist yet. Start with an empty
// map; `finalize` creates the file.
Default::default()
}
Err(e) => {
panic!(
"LocalPersister: Unable to read the {} file: {}",
config.file.display(),
e,
)
}
};
Self { config, query_map }
}
fn hash_operation(&self, operation_text: String) -> String {
match self.config.algorithm {
LocalPersistAlgorithm::MD5 => {
let mut md5 = Md5::new();
md5.update(operation_text);
hex::encode(md5.finalize())
}
LocalPersistAlgorithm::SHA1 => {View on GitHub (pinned to 668b1b85e0)
Solutions
- Fix permissions on the persisted operations file/directory (chmod/chown, especially for root-created Docker artifacts).
- Ensure the configured path is a file, not a directory.
- Delete the corrupt/unreadable file to re-initialize state (it is recreated on finalize).
- Check the filesystem is writable/readable for the user running the compiler.
Example fix
// before
persistConfig: { file: __dirname + '/persisted' } // is a directory
// after
persistConfig: { file: __dirname + '/persisted/queries.json' } Defensive patterns
Strategy: validation
Validate before calling
const st = fs.statSync(persistFile);
if (!st.isFile()) throw new Error('persist path must be a file');
fs.accessSync(persistFile, fs.constants.R_OK | fs.constants.W_OK); Type guard
function isReadableFile(p) { try { return fs.statSync(p).isFile() && fs.accessSync(p, fs.constants.R_OK) === undefined; } catch { return false; } } Try / catch
try { startRelayPersist(config); } catch (e) { if (/LocalPersister: Unable to read/.test(String(e))) { fs.rmSync(persistFile, { force: true }); startRelayPersist(config); } else { throw e; } } Prevention
- Ensure the persisted-queries path is a file, not a directory
- Fix ownership of artifact dirs created by Docker (run as same UID or chown)
- Check filesystem writability in CI before running the compiler
When it happens
Trigger: Instantiating LocalPersister with a config.file path that exists but cannot be read: permission denied, path is a directory, or an IO error other than NotFound occurs while opening it.
Common situations: artifactDirectory owned by another user (e.g. created by Docker as root); the persisted-queries path accidentally set to a directory; read-only filesystem in CI.
Related errors
- Failed to read glob entry: {}
- Unable to canonicalize file {:?}. Error: {:?}
- Expect to be able to strip common_path from {:?} {:?}
- Unable to get current working directory.
- Duplicate fragment definitions named {}: first one: {:?} s
AI-assisted analysis of facebook/relay@668b1b85e0 (2026-09-02).
Data as JSON: /api/errors/359a6361e8a591ae.
Report an issue: GitHub.