yiisoft/yii2 · error · yii\base\Exception

Failed to create directory "{$path}": {message}

Error message

Failed to create directory "{$path}": {message}

What it means

Thrown by yii\helpers\BaseFileHelper::createDirectory() when the native PHP mkdir() call raised an exception and a follow-up is_dir($path) check confirms the directory still does not exist. Yii wraps the low-level failure (permission denied, path conflicts, open_basedir restriction, disk full) into a yii\base\Exception carrying the OS message. The is_dir() re-check exists because of yii2 issue #9288: if a concurrent process created the directory between mkdir() failing and the check, the error is tolerated instead of thrown.

Source

Thrown at framework/helpers/BaseFileHelper.php:720

     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function createDirectory($path, $mode = 0775, $recursive = true)
    {
        if (is_dir($path)) {
            return true;
        }
        $parentDir = dirname($path);
        // recurse if parent dir does not exist and we are not at the root of the file system.
        if ($recursive && !is_dir($parentDir) && $parentDir !== $path) {
            static::createDirectory($parentDir, $mode, true);
        }
        try {
            if (!mkdir($path, $mode)) {
                return false;
            }
        } catch (\Exception $e) {
            if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288
                throw new \yii\base\Exception("Failed to create directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
            }
        }
        try {
            return chmod($path, $mode);
        } catch (\Exception $e) {
            throw new \yii\base\Exception("Failed to change permissions for directory \"$path\": " . $e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Performs a simple comparison of file or directory names.
     *
     * Based on match_basename() from dir.c of git 1.8.5.3 sources.
     *
     * @param string $baseName file or directory name to compare with the pattern
     * @param string $pattern the pattern that $baseName will be compared against
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern
     * @param int $flags pattern flags

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Check the OS message in the exception, then fix ownership/permissions of the parent: chown -R www-data:www-data <parent> && chmod -R 775 <parent> so the PHP process user can write.
  2. Verify $path is not already taken by a regular file (file_exists($path) && !is_dir($path)); remove the file or correct the path/alias (e.g. @runtime/cache vs @runtime/cache/x).
  3. If the path is outside the allowed tree, adjust open_basedir in php.ini/fpm pool config, or move the target inside an allowed alias such as @runtime.
  4. Pass $recursive = true (the default in FileHelper::createDirectory is false, unlike the native mkdir wrapper) when intermediate directories may be missing.
  5. In containers, confirm the volume is mounted rw and the container user matches the volume owner; docker-compose down/up after fixing the host-side ownership.

Example fix

// before
FileHelper::createDirectory(Yii::getAlias('@runtime'), 0777); // parent owned by root -> mkdir() warns -> exception

// after (host): give the PHP user write access to the parent
// sudo chown -R www-data:www-data /var/www/app/runtime && sudo chmod -R 775 /var/www/app/runtime
FileHelper::createDirectory(Yii::getAlias('@runtime'), 0775);
Defensive patterns

Strategy: validation

Validate before calling

$path = Yii::getAlias('@runtime/cache');
$parent = dirname($path);
if (file_exists($path) && !is_dir($path)) {
    throw new RuntimeException("Path is occupied by a regular file: $path");
}
if (!is_dir($parent) && !is_writable(dirname($parent)) === false && !is_dir($parent)) {
    // rely on recursive creation only when the nearest existing ancestor is writable
}
$nearest = $parent;
while (!is_dir($nearest)) { $nearest = dirname($nearest); }
if (!is_writable($nearest)) {
    throw new RuntimeException("Cannot create directories under $nearest: not writable by " . get_current_user());
}
FileHelper::createDirectory($path, 0775, true);

Type guard

/** @psalm-assert !null *//* narrow to a path that can actually be created */
function canCreateDirectory(string $path): bool
{
    if (file_exists($path)) {
        return is_dir($path); // existing dir is fine; existing file is not
    }
    $nearest = dirname($path);
    while (!is_dir($nearest)) {
        $nearest = dirname($nearest);
    }
    return is_writable($nearest);
}

Try / catch

try {
    if (!FileHelper::createDirectory($path, 0775, true) && !is_dir($path)) {
        throw new RuntimeException("mkdir returned false for $path");
    }
} catch (\yii\base\Exception $e) {
    if (is_dir($path)) {
        Yii::warning("Directory appeared concurrently: {$e->getMessage()}", __METHOD__);
    } else {
        Yii::error("Cannot create $path: {$e->getMessage()}", __METHOD__);
        throw $e; // surface the real cause (perms / open_basedir) to ops
    }
}

Prevention

When it happens

Trigger: Calling FileHelper::createDirectory($path, $mode, $recursive) when: the parent directory is not writable by the PHP process user; $path already exists as a regular file; $path lies outside an open_basedir restriction; $recursive is false and the parent chain is missing; the disk or inode table is full; or an error handler (e.g. Yii's own, or a custom set_error_handler) converts mkdir()'s E_WARNING into an ErrorException, which is what makes the catch block reachable at all.

Common situations: Deployments where runtime/cache/assets dirs (e.g. @runtime, @webroot/assets) are owned by the deploy/CLI user but the site runs as www-data or php-fpm; shared hosting with open_basedir; Docker containers with read-only or wrongly-owned volume mounts; a stale file occupying a directory path after a broken deploy; permission modes reset by umask or by rsync without -p.

Related errors


AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17). Data as JSON: /api/errors/9efae28b12abb415. Report an issue: GitHub.