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

  1. Check existence before reading: `if (!await fsDriver.exists(path)) return [];`.
  2. If using a scoped URI, ensure the handle was granted via `mountExternalDirectory` and not revoked.
  3. Re-prompt the user to re-share the folder when the SAF permission was revoked.
  4. 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

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


AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12). Data as JSON: /api/errors/777a2f26815e6af2. Report an issue: GitHub.