laurent22/joplin · error · Error
Could not read directory: ${path}: ${error.message}
Error message
Could not read directory: ${path}: ${error.message} What it means
Thrown by the React Native fs-driver's `readDirStats`/directory read path when the underlying `RNSAF.listFiles` (scoped storage URI) or `RNFS.readDir` (filesystem path) throws. The path and the native error's message are interpolated. It wraps any native read failure into a single, identifiable error.
Source
Thrown at packages/app-mobile/utils/fs-driver/fs-driver-rn.ts:126
path: path,
size: stat.size,
};
}
public async readDirStats(path: string, options: ReadDirStatsOptions = null) {
if (!options) options = { recursive: false };
const isScoped = isScopedUri(path);
let stats: RnfsStatLike[] = [];
try {
if (isScoped) {
stats = await RNSAF.listFiles(path);
} else {
stats = await RNFS.readDir(path);
}
} catch (error) {
throw new Error(`Could not read directory: ${path}: ${error.message}`);
}
const toRelativePath = (stat: RnfsStatLike) => {
let relativePath = isScoped ? (stat as DocumentFileDetail).uri : (stat as StatResultT | ReadDirResItemT).path;
// Workaround: Paths returned by RNFS.readDir can include a leading /private/, when this isn't included
// in the original path variable:
if (relativePath.startsWith('/private/') && !path.startsWith('/private/')) {
relativePath = relativePath.replace(/^\/private/, '');
}
if (!relativePath.startsWith(path)) {
logger.warn('readDirStats: Relative path does not start with original:', { relativePath, path });
}
relativePath = relativePath.substring(path.length + 1);
return relativePath;
};View on GitHub (pinned to 2654b33620)
Solutions
- Check existence before reading: `if (!await fsDriver.exists(path)) return [];`.
- If using a scoped URI, ensure the handle was granted via `mountExternalDirectory` and not revoked.
- Re-prompt the user to re-share the folder when the SAF permission was revoked.
- Handle the `/private/` prefix mismatch by normalizing paths before comparison.
Example fix
// before
const stats = await shim.fsDriver().readDirStats(path);
// after
if (!await shim.fsDriver().exists(path)) return [];
try {
return await shim.fsDriver().readDirStats(path);
} catch (error) {
logger.warn('Directory read failed, may need re-share:', path, error.message);
return [];
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!await shim.fsDriver().exists(path)) {
logger.info('Directory does not exist:', path);
return [];
} Type guard
null
Try / catch
try {
return await shim.fsDriver().readDirStats(path);
} catch (error) {
if (/Could not read directory/i.test(error.message)) {
logger.warn('Read failed; may need to re-share folder:', path);
return [];
}
throw error;
} Prevention
- Check directory existence before reading.
- Re-share scoped-storage folders when SAF permission is revoked.
- Normalize /private/ prefix mismatches before path comparison.
When it happens
Trigger: Calling a directory-read API on a path that does not exist, is not a directory, lacks permission, or whose scoped-storage handle has been revoked. Also reached if the path is malformed for the chosen backend (non-URI string passed to the scoped branch, or vice-versa).
Common situations: The user revoked the SAF folder permission after granting it; the directory was deleted out-of-band; an SD-card unmount; a path that mixes `/private/` prefix forms (note the workaround below this throw); Android scoped storage restrictions.
Related errors
- Missing read-write access. It might be necessary to share th
- ${mode} access is needed for ${id}.
- No model found at path: ${JSON.stringify(modelFolderPath)}
- Model not found at path ${modelPath}
- Error! Destination already exists
AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12).
Data as JSON: /api/errors/777a2f26815e6af2.
Report an issue: GitHub.