Input and output can fail.
A file may not exist. A disk may be full. A directory may be missing. A program may not have permission to read or write. A pipe may be closed by the program on the other end.
Zig makes these cases part of the type.
pub fn main() !void {
const cwd = std.fs.cwd();
const file = try cwd.openFile("input.txt", .{});
defer file.close();
var buffer: [128]u8 = undefined;
const n = try file.read(&buffer);
try std.io.getStdOut().writeAll(buffer[0..n]);
}The return type is:
!voidThis means the function either completes normally with void, or returns an error.
The expression:
try cwd.openFile("input.txt", .{})means: if opening the file succeeds, use the file. If it fails, return the error from the current function.
This is the common style for small programs. It keeps the main path clear, but does not discard errors.
Sometimes a program should handle an error directly.
const file = cwd.openFile("input.txt", .{}) catch |err| {
try std.io.getStdErr().writer().print(
"cannot open input.txt: {}\n",
.{err},
);
return err;
};The catch branch receives the error value in err.
This lets the program print a better diagnostic before returning.
Different errors can be handled differently with switch.
const file = cwd.openFile("input.txt", .{}) catch |err| switch (err) {
error.FileNotFound => {
try std.io.getStdErr().writeAll(
"input.txt does not exist\n",
);
return err;
},
error.AccessDenied => {
try std.io.getStdErr().writeAll(
"permission denied\n",
);
return err;
},
else => return err,
};This is useful near the boundary of a program, where errors become messages for users.
Inside lower-level functions, returning errors is often better.
fn copyFile(src_name: []const u8, dst_name: []const u8) !void {
const cwd = std.fs.cwd();
const src = try cwd.openFile(src_name, .{});
defer src.close();
const dst = try cwd.createFile(dst_name, .{});
defer dst.close();
var buffer: [4096]u8 = undefined;
while (true) {
const n = try src.read(&buffer);
if (n == 0) break;
try dst.writeAll(buffer[0..n]);
}
}The function does not decide how to report errors. It only says that copying may fail.
The caller decides what to do.
pub fn main() !void {
copyFile("input.txt", "output.txt") catch |err| {
try std.io.getStdErr().writer().print(
"copy failed: {}\n",
.{err},
);
return err;
};
}This separation is important. Low-level code should usually preserve the error. Top-level code should usually explain the error.
Cleanup must still happen when errors occur. defer handles normal cleanup.
const file = try cwd.openFile("input.txt", .{});
defer file.close();If a later try returns an error, file.close() still runs.
For cleanup that should happen only on error, use errdefer.
const dst = try cwd.createFile("output.txt", .{});
errdefer cwd.deleteFile("output.txt") catch {};
defer dst.close();Here, if the function fails after creating output.txt, the partial file is removed. If the function succeeds, errdefer does not run.
This pattern is common when writing output files:
fn writeCompleteFile(name: []const u8, data: []const u8) !void {
const cwd = std.fs.cwd();
const file = try cwd.createFile(name, .{});
errdefer cwd.deleteFile(name) catch {};
defer file.close();
try file.writeAll(data);
}If writeAll fails, the incomplete file is deleted.
I/O code should follow a few rules:
- return errors from low-level functions
- print diagnostics at program boundaries
- use
deferfor resource cleanup - use
errdeferfor rollback - never assume a file operation succeeds
Exercise 13-21. Modify the file copy program so it prints file not found for error.FileNotFound.
Exercise 13-22. Write a function that opens a file and returns its first byte.
Exercise 13-23. Change the function so it returns null for an empty file.
Exercise 13-24. Write an output function that deletes the output file if writing fails.