Ambiguous Error Codes from FileManager.createDirectory When Intermediate Path Is a Regular File
0 reputation · 19 Aug 2021, 09:22 UTC
0 reputation · 19 Aug 2021, 09:22 UTC
On Darwin and swift-corelibs-foundation, FileManager.createDirectory(atPath:withIntermediateDirectories:attributes:) fails when an intermediate path component already exists as a regular file, even if withIntermediateDirectories is true. Apple's documentation does not fully specify which NSError code should be returned for this condition, leaving developers uncertain whether to expect NSFileWriteFileExistsError, NSFileReadNoSuchFileError, or another value.
This ambiguity is compounded by cross-platform differences: swift-corelibs-foundation has historically reported different error codes for the same filesystem conflict, and exact error domains or names may vary by OS version. In automated deployment or build pipelines, a script may assume a directory tree exists while an earlier step created a regular file at one of the intermediate paths, causing directory creation to fail unexpectedly.
Given that the underlying POSIX errno may be the only stable signal across platforms, which error code should production code handle as the primary case? Is there a documented contract for FileManager that specifies the exact NSError domain and code for an intermediate regular-file conflict? Should developers inspect the underlying errno instead of matching against Foundation error constants?
35275 reputation · 19 Aug 2021, 10:50 UTC
Handle ENOTDIR (errno 20) via the underlying POSIX error as your primary signal. Do not rely on Foundation error constants like NSFileWriteFileExistsError or NSFileReadNoSuchFileError — they vary across Darwin, iOS, and swift-corelibs-foundation versions, and Apple publishes no contract guaranteeing a specific NSError code for this scenario.
NSCocoaErrorDomain / NSFileWriteFileExistsError (516), but older OS releases have returned NSFileReadNoSuchFileError (4) or NSFileWriteNoPermissionError (513) for the same filesystem state.NSFileReadNoSuchFileError or a generic NSFileWriteError because the kernel returns ENOTDIR (not EEXIST) when mkdir(2) hits a regular file, and the Foundation errno-to-NSError mapping differs from Darwin's.The POSIX mkdir(2) syscall returns ENOTDIR (20) when a path component exists as a non-directory. This behavior is specified by POSIX and implemented consistently by both XNU (Darwin) and Linux kernels. Foundation wraps this errno but translates it to different high-level codes depending on the platform and version.
do {
try FileManager.default.createDirectory(atPath: path,
withIntermediateDirectories: true,
attributes: nil)
} catch let error as NSError {
// Prefer the underlying POSIX errno
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError,
underlying.domain == NSPOSIXErrorDomain,
underlying.code == ENOTDIR {
// Intermediate component is a regular file (or symlink to one)
handleIntermediateFileConflict(at: path)
return
}
// Fallback: some older Foundation versions surface ENOTDIR directly
if error.domain == NSPOSIXErrorDomain && error.code == ENOTDIR {
handleIntermediateFileConflict(at: path)
return
}
// Other failures (permissions, disk full, etc.)
throw error
}
Run this minimal test on each target OS / swift-corelibs-foundation version you support:
import Foundation
let fm = FileManager.default
let base = NSTemporaryDirectory().appending("test-intermediate-file")
try? fm.removeItem(atPath: base)
try fm.createDirectory(atPath: base, withIntermediateDirectories: true)
// Create a regular file where a directory would be created next
let intermediate = (base as NSString).appendingPathComponent("regular-file")
try "data".write(toFile: intermediate, atomically: true, encoding: .utf8)
// Now ask for a deeper directory
let target = (intermediate as NSString).appendingPathComponent("subdir")
do {
try fm.createDirectory(atPath: target, withIntermediateDirectories: true)
} catch let e as NSError {
print("Domain: \(e.domain), Code: \(e.code)")
if let u = e.userInfo[NSUnderlyingErrorKey] as? NSError {
print("Underlying domain: \(u.domain), code: \(u.code)")
}
}
Confirm that the underlying error's domain is NSPOSIXErrorDomain and code is 20 (ENOTDIR) on every platform you ship.
If your code runs inside a macOS/iOS App Sandbox, the sandbox layer can wrap the filesystem error and surface a different NSError (e.g., NSFileWriteNoPermissionError) even when the underlying issue is ENOTDIR. In that case, also check error.userInfo[NSFilePathErrorKey] to locate the conflicting component and verify its type with FileManager.attributesOfItem(atPath:) before deciding how to recover.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.