mastra-ai/mastra · error · NotDirectoryError

ENOTDIR

ENOTDIR

Error message

Path is not a directory: ${path}

What it means

writeFile throws NotDirectoryError (code ENOTDIR) when options.recursive === false and the parent directory of the target path exists but is a regular file, not a directory. In non-recursive mode the library requires the immediate parent to be an existing directory; anything else is rejected before writing. (If the parent is missing entirely you get DirectoryNotFoundError instead.)

Source

Thrown at packages/core/src/workspace/filesystem/local-filesystem.ts:425

    }
  }

  async writeFile(inputPath: string, content: FileContent, options?: WriteOptions): Promise<void> {
    const contentSize = Buffer.isBuffer(content) ? content.length : content.length;
    this.logger.debug('Writing file', { path: inputPath, size: contentSize, recursive: options?.recursive });
    await this.ensureReady();
    this.assertWritable('writeFile');
    const absolutePath = this.resolvePath(inputPath);
    await this.assertPathContained(absolutePath);

    // When recursive is explicitly false, verify parent directory exists
    if (options?.recursive === false) {
      const dir = nodePath.dirname(absolutePath);
      const parentPath = nodePath.dirname(inputPath);
      try {
        const stat = await fs.stat(dir);
        if (!stat.isDirectory()) {
          throw new NotDirectoryError(parentPath);
        }
      } catch (error: unknown) {
        if (error instanceof NotDirectoryError) throw error;
        if (isEnoentError(error)) {
          throw new DirectoryNotFoundError(parentPath);
        }
        throw error;
      }
    }

    if (options?.recursive !== false) {
      const dir = nodePath.dirname(absolutePath);
      await fs.mkdir(dir, { recursive: true });
    }

    // Optimistic concurrency: reject if file was modified since caller last read it
    if (options?.expectedMtime) {
      try {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Remove recursive: false (or set recursive: true) so missing parent directories are created automatically — this is the default.
  2. Rename/delete the file that is blocking the directory name, then write again.
  3. Verify with stat that each path segment used as a directory is actually a directory.
  4. Choose a target path whose parent is a real directory.

Example fix

// before
await fs.writeFile('data/child.txt', 'x', { recursive: false }); // ENOTDIR if 'data' is a file
// after
await fs.writeFile('data/child.txt', 'x'); // recursive defaults to true; or free the 'data' name first
Defensive patterns

Strategy: validation

Validate before calling

const parent = nodePath.posix.dirname(p);
const s = await ws.stat(parent);
if (s.type !== 'directory') {
  throw new Error(`${parent} is not a directory; fix the path or enable recursive writes`);
}

Type guard

import { NotDirectoryError } from '@mastra/core/workspace/errors';
function isNotDirectoryError(e: unknown): e is NotDirectoryError {
  return e instanceof NotDirectoryError ||
    (e instanceof Error && 'code' in e && (e as { code?: string }).code === 'ENOTDIR');
}

Try / catch

try {
  await ws.writeFile(p, data, { recursive: false });
} catch (e) {
  if (isNotDirectoryError(e)) {
    await ws.writeFile(p, data); // fall back to recursive (auto-mkdir)
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: writeFile('a/b.txt', data, { recursive: false }) where 'a' is an existing file; path segments colliding, e.g. a file named 'data' already exists and you write 'data/child.txt'; passing recursive: false explicitly when the caller assumed directories are auto-created.

Common situations: A previously written file occupies the name now used as a directory; workspace was seeded with a flat file layout that conflicts with nested paths; copy-pasted options keeping recursive: false from a strict-mode call.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/c99b5f9b065ce5c8. Report an issue: GitHub.