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 flagsView on GitHub (pinned to 66f00d18a2)
Solutions
- 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.
- 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).
- 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.
- Pass $recursive = true (the default in FileHelper::createDirectory is false, unlike the native mkdir wrapper) when intermediate directories may be missing.
- 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
- Provision runtime/cache/assets directories in deployment (Ansible, Dockerfile) with the web user as owner instead of creating them on first request.
- After every deploy, run a permissions check: the nearest existing ancestor of every runtime path must be writable by the PHP-FPM user.
- Always expand path aliases (Yii::getAlias) before calling createDirectory so you log the real filesystem path, not '@runtime/...'.
- Pass $recursive = true whenever the parent chain may be missing; the default false surprises ex-Symfony users.
- Include is_dir($path) in monitoring: an existing directory proves the operation already succeeded (the #9288 race) and needs no retry.
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
- Failed to change permissions for directory "{$path}": {messa
- The view file does not exist: $viewFile
- FileDependency::fileName must be set
- MemCache requires PHP $extension extension to be loaded.
- Unable to open directory: $dir
AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17).
Data as JSON: /api/errors/9efae28b12abb415.
Report an issue: GitHub.