11. Files & error handling¶
What & why¶
Real programs read and write files, and real I/O fails: the file is missing, the disk is full, the data is malformed. Swift's error handling is explicit — functions that can fail are marked throws, and callers must acknowledge it with try. Nothing is thrown "invisibly."
Defining and throwing errors¶
Errors are usually enums conforming to Error:
enum ParseError: Error {
case empty
case notANumber(String)
case negative(Int)
}
func parsePositive(_ text: String) throws -> Int {
guard !text.isEmpty else { throw ParseError.empty }
guard let n = Int(text) else { throw ParseError.notANumber(text) }
guard n >= 0 else { throw ParseError.negative(n) }
return n
}
throws goes before the return arrow. throw leaves the function immediately.
Catching errors: do–catch¶
do {
let n = try parsePositive("42")
print("Got \(n)")
let m = try parsePositive("-7") // throws here...
print("Got \(m)") // ...so this line never runs
} catch ParseError.empty {
print("Input was empty")
} catch ParseError.notANumber(let text) {
print("'\(text)' is not a number")
} catch {
print("Other error: \(error)") // `error` is implicit in a bare catch
}
Every call to a throwing function needs try (or a variant), and it must sit in a do block with a catch, or inside another throwing function.
try?, try!, and propagating¶
let a = try? parsePositive("12") // Int? — nil if it threw (error is discarded)
let b = try! parsePositive("12") // crashes if it throws — only for "can't fail" cases
func sumAll(_ texts: [String]) throws -> Int { // propagate: caller deals with it
var total = 0
for t in texts {
total += try parsePositive(t)
}
return total
}
Rule of thumb: handle errors where you can do something useful about them, and propagate everywhere else.
defer — cleanup that always runs¶
func work() throws {
print("open")
defer { print("close") } // runs when the scope exits — normally OR by throw/return
try riskyStep()
print("finished")
}
func riskyStep() throws { throw ParseError.empty }
Several defers run in reverse order. Use them to release resources next to where you acquire them.
Making errors descriptive¶
Conform to LocalizedError for human-friendly messages:
import Foundation
enum ConfigError: LocalizedError {
case missing(String)
var errorDescription: String? {
switch self {
case .missing(let key): return "Missing config key: \(key)"
}
}
}
do { throw ConfigError.missing("port") }
catch { print(error.localizedDescription) } // Missing config key: port
Reading and writing files¶
File APIs live in Foundation, which works on macOS, Linux, and Windows.
import Foundation
let path = "notes.txt"
// Write
try "first line\nsecond line\n".write(toFile: path, atomically: true, encoding: .utf8)
// Read the whole file
let contents = try String(contentsOfFile: path, encoding: .utf8)
let lines = contents.split(separator: "\n")
print(lines.count) // 2
Using URLs (preferred for new code):
let url = URL(fileURLWithPath: "notes.txt")
let text = try String(contentsOf: url, encoding: .utf8)
try text.uppercased().write(to: url, atomically: true, encoding: .utf8)
Binary data uses Data:
let data = try Data(contentsOf: url)
print(data.count)
try data.write(to: URL(fileURLWithPath: "copy.bin"))
Handling "file not found" gracefully¶
do {
let s = try String(contentsOfFile: "missing.txt", encoding: .utf8)
print(s)
} catch {
print("Could not read file: \(error.localizedDescription)")
}
Files and directories¶
let fm = FileManager.default
print(fm.currentDirectoryPath)
print(fm.fileExists(atPath: "notes.txt"))
try fm.createDirectory(atPath: "out/logs", withIntermediateDirectories: true)
let entries = try fm.contentsOfDirectory(atPath: ".")
print(entries.sorted())
try fm.removeItem(atPath: "out") // deletes the directory tree — be careful!
Build paths with URL, not string concatenation:
let dir = URL(fileURLWithPath: "data", isDirectory: true)
let file = dir.appendingPathComponent("scores.csv")
print(file.path, file.lastPathComponent, file.pathExtension) // .../scores.csv scores.csv csv
Command-line arguments & exit codes¶
import Foundation
let args = CommandLine.arguments // [0] is the program path
guard args.count >= 2 else {
FileHandle.standardError.write(Data("usage: wordcount <file>\n".utf8))
exit(2)
}
let text = try String(contentsOfFile: args[1], encoding: .utf8)
print(text.split(whereSeparator: \.isWhitespace).count)
Top-level try is allowed in main.swift; an uncaught error ends the program with a message. Convention: exit code 0 = success, non-zero = failure.
Result — errors as values¶
Sometimes you want to store or pass a success-or-failure outcome:
let outcomes = ["5", "x", "-1"].map { text in
Result { try parsePositive(text) } // Result<Int, any Error>
}
for outcome in outcomes {
switch outcome {
case .success(let n): print("ok", n)
case .failure(let e): print("failed:", e)
}
}
Prefer plain throws for new synchronous code; Result shines when you need to store or pass along outcomes (Intermediate covers it properly).
Common mistakes¶
- Writing
trywithout ado/catchor an enclosingthrows— the compiler will insist. - Swallowing errors with
try?everywhere, losing the reason a failure occurred. - Using
try!on I/O — files go missing. Handle it. - Catch-all
catch { }that hides the error. At least print it. - Building paths with
+ "/" +— breaks on Windows; useURL.appendingPathComponent. - Forgetting that relative paths depend on the current working directory (
swift runruns from the package root). - Forgetting
import Foundationfor file APIs.
Exercises¶
- Write
func readNumbers(from path: String) throws -> [Int]that reads a file of one integer per line and throws a custom error naming the first bad line. - Write a
wordcounttool that takes a file path argument and prints lines, words, and characters. Exit with code 2 and a usage message if no argument is given. - Write a function that copies a file, and use
deferto print a "done" message regardless of failure. - Write
safeRead(_ path: String) -> String?usingtry?, then a second version that returnsResult<String, Error>. Which tells the caller more? - List all
.swiftfiles under a directory recursively usingFileManager.default.enumerator(atPath:). - Make an error enum with a
LocalizedErrordescription, throw it from three different places, and catch each case separately.
Recap¶
- Mark failing functions
throws; call them withtryinsidedo/catch(or anotherthrowsfunction). try?→ optional,try!→ crash on error; use them sparingly.deferguarantees cleanup; conform errors toLocalizedErrorfor friendly messages.- Foundation provides
String(contentsOfFile:),Data,URL, andFileManager. - Use
CommandLine.argumentsandexit(_:)for command-line tools.
Next: Capstone: task tracker →