diff --git a/README.md b/README.md
index 29a917bdd528fc91140d29c03e3c3f6eeeb54f11..d5dbe392ad738ac48588a7e1eaa7b94755ba0835 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,6 @@ Missing features:
 - Directory refresh
 - File deletion
 - Opening a shell
-- OOM handling
 
 ### Improvements compared to the C version
 
diff --git a/src/browser.zig b/src/browser.zig
index 042537e9423385e557eeeea486a32751f9954dbb..7ed05f734919caec132c8bc5a7d3882609fdfff1 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -120,24 +120,24 @@ fn sortDir() void {
 // - dir_parents changes (i.e. we change directory)
 // - config.show_hidden changes
 // - files in this dir have been added or removed
-pub fn loadDir() !void {
+pub fn loadDir() void {
     dir_items.shrinkRetainingCapacity(0);
     dir_max_size = 1;
     dir_max_blocks = 1;
 
     if (dir_parents.top() != model.root)
-        try dir_items.append(null);
+        dir_items.append(null) catch unreachable;
     var it = dir_parents.top().sub;
     while (it) |e| {
         if (e.blocks > dir_max_blocks) dir_max_blocks = e.blocks;
         if (e.size > dir_max_size) dir_max_size = e.size;
         if (main.config.show_hidden) // fast path
-            try dir_items.append(e)
+            dir_items.append(e) catch unreachable
         else {
             const excl = if (e.file()) |f| f.excluded else false;
             const name = e.name();
             if (!excl and name[0] != '.' and name[name.len-1] != '~')
-                try dir_items.append(e);
+                dir_items.append(e) catch unreachable;
         }
         it = e.next;
     }
@@ -271,19 +271,19 @@ const Row = struct {
             ui.addstr("                 no mtime");
     }
 
-    fn name(self: *Self) !void {
+    fn name(self: *Self) void {
         ui.move(self.row, self.col);
         if (self.item) |i| {
             self.bg.fg(if (i.etype == .dir) .dir else .default);
             ui.addch(if (i.etype == .dir) '/' else ' ');
-            ui.addstr(try ui.shorten(try ui.toUtf8(i.name()), saturateSub(ui.cols, self.col + 1)));
+            ui.addstr(ui.shorten(ui.toUtf8(i.name()), saturateSub(ui.cols, self.col + 1)));
         } else {
             self.bg.fg(.dir);
             ui.addstr("/..");
         }
     }
 
-    fn draw(self: *Self) !void {
+    fn draw(self: *Self) void {
         if (self.bg == .sel) {
             self.bg.fg(.default);
             ui.move(self.row, 0);
@@ -294,7 +294,7 @@ const Row = struct {
         self.graph();
         self.items();
         self.mtime();
-        try self.name();
+        self.name();
     }
 };
 
@@ -314,7 +314,7 @@ fn drawQuit() void {
     ui.addch(')');
 }
 
-pub fn draw() !void {
+pub fn draw() void {
     ui.style(.hd);
     ui.move(0,0);
     ui.hline(' ', ui.cols);
@@ -340,8 +340,8 @@ pub fn draw() !void {
     ui.style(.dir);
 
     var pathbuf = std.ArrayList(u8).init(main.allocator);
-    try dir_parents.path(pathbuf.writer());
-    ui.addstr(try ui.shorten(try ui.toUtf8(try arrayListBufZ(&pathbuf)), saturateSub(ui.cols, 5)));
+    dir_parents.path(&pathbuf);
+    ui.addstr(ui.shorten(ui.toUtf8(arrayListBufZ(&pathbuf)), saturateSub(ui.cols, 5)));
     pathbuf.deinit();
 
     ui.style(.default);
@@ -361,7 +361,7 @@ pub fn draw() !void {
             .bg = if (i+current_view.top == cursor_idx) .sel else .default,
         };
         if (row.bg == .sel) sel_row = i+2;
-        try row.draw();
+        row.draw();
     }
 
     ui.style(.hd);
@@ -389,7 +389,7 @@ fn sortToggle(col: main.config.SortCol, default_order: main.config.SortOrder) vo
     sortDir();
 }
 
-pub fn keyInput(ch: i32) !void {
+pub fn keyInput(ch: i32) void {
     if (need_confirm_quit) {
         switch (ch) {
             'y', 'Y' => if (need_confirm_quit) ui.quit(),
@@ -422,7 +422,7 @@ pub fn keyInput(ch: i32) !void {
         'M' => if (main.config.extended) sortToggle(.mtime, .desc),
         'e' => {
             main.config.show_hidden = !main.config.show_hidden;
-            try loadDir();
+            loadDir();
         },
         't' => {
             main.config.sort_dirsfirst = !main.config.sort_dirsfirst;
@@ -445,18 +445,18 @@ pub fn keyInput(ch: i32) !void {
             if (dir_items.items.len == 0) {
             } else if (dir_items.items[cursor_idx]) |e| {
                 if (e.dir()) |d| {
-                    try dir_parents.push(d);
-                    try loadDir();
+                    dir_parents.push(d);
+                    loadDir();
                 }
             } else if (dir_parents.top() != model.root) {
                 dir_parents.pop();
-                try loadDir();
+                loadDir();
             }
         },
         'h', '<', ui.c.KEY_BACKSPACE, ui.c.KEY_LEFT => {
             if (dir_parents.top() != model.root) {
                 dir_parents.pop();
-                try loadDir();
+                loadDir();
             }
         },
 
diff --git a/src/main.zig b/src/main.zig
index e0bdba643dfae28ab943cf7d530ebae7a4de6aa8..195e3d9a1a0f03a8c24f0a3878e50db01dc8aeef 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -7,7 +7,29 @@ const ui = @import("ui.zig");
 const browser = @import("browser.zig");
 const c = @cImport(@cInclude("locale.h"));
 
-pub const allocator = std.heap.c_allocator;
+// "Custom" allocator that wraps the libc allocator and calls ui.oom() on error.
+// This allocator never returns an error, it either succeeds or causes ncdu to quit.
+// (Which means you'll find a lot of "catch unreachable" sprinkled through the code,
+// they look scarier than they are)
+fn wrapAlloc(alloc: *std.mem.Allocator, len: usize, alignment: u29, len_align: u29, return_address: usize) error{OutOfMemory}![]u8 {
+    while (true) {
+        if (std.heap.c_allocator.allocFn(alloc, len, alignment, len_align, return_address)) |r|
+            return r
+        else |_| {}
+        ui.oom();
+    }
+}
+
+fn wrapResize(alloc: *std.mem.Allocator, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, return_address: usize) std.mem.Allocator.Error!usize {
+    // AFAIK, all uses of resizeFn to grow an allocation will fall back to allocFn on failure.
+    return std.heap.c_allocator.resizeFn(alloc, buf, buf_align, new_len, len_align, return_address);
+}
+
+var allocator_state = std.mem.Allocator{
+    .allocFn = wrapAlloc,
+    .resizeFn = wrapResize,
+};
+pub const allocator = &allocator_state;
 
 pub const config = struct {
     pub const SortCol = enum { name, blocks, size, items, mtime };
@@ -158,7 +180,7 @@ fn readExcludeFile(path: []const u8) !void {
         rd.readUntilDelimiterArrayList(&buf, '\n', 4096)
             catch |e| if (e != error.EndOfStream) return e else if (buf.items.len == 0) break;
         if (buf.items.len > 0)
-            try config.exclude_patterns.append(try buf.toOwnedSliceSentinel(0));
+            config.exclude_patterns.append(buf.toOwnedSliceSentinel(0) catch unreachable) catch unreachable;
     }
 }
 
@@ -203,7 +225,7 @@ pub fn main() !void {
         else if(opt.is("-f")) import_file = args.arg()
         else if(opt.is("--si")) config.si = true
         else if(opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true
-        else if(opt.is("--exclude")) try config.exclude_patterns.append(args.arg())
+        else if(opt.is("--exclude")) config.exclude_patterns.append(args.arg()) catch unreachable
         else if(opt.is("-X") or opt.is("--exclude-from")) {
             const arg = args.arg();
             readExcludeFile(arg) catch |e| ui.die("Error reading excludes from {s}: {}.\n", .{ arg, e });
@@ -249,22 +271,21 @@ pub fn main() !void {
     config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
     ui.init();
     state = .browse;
-    try browser.loadDir();
+    browser.loadDir();
 
-    // TODO: Handle OOM errors
-    while (true) try handleEvent(true, false);
+    while (true) handleEvent(true, false);
 }
 
 var event_delay_timer: std.time.Timer = undefined;
 
 // Draw the screen and handle the next input event.
 // In non-blocking mode, screen drawing is rate-limited to keep this function fast.
-pub fn handleEvent(block: bool, force_draw: bool) !void {
+pub fn handleEvent(block: bool, force_draw: bool) void {
     if (block or force_draw or event_delay_timer.read() > config.update_delay) {
         if (ui.inited) _ = ui.c.erase();
         switch (state) {
-            .scan => try scan.draw(),
-            .browse => try browser.draw(),
+            .scan => scan.draw(),
+            .browse => browser.draw(),
         }
         if (ui.inited) _ = ui.c.refresh();
         event_delay_timer.reset();
@@ -280,8 +301,8 @@ pub fn handleEvent(block: bool, force_draw: bool) !void {
         if (ch == 0) return;
         if (ch == -1) return handleEvent(firstblock, true);
         switch (state) {
-            .scan => try scan.keyInput(ch),
-            .browse => try browser.keyInput(ch),
+            .scan => scan.keyInput(ch),
+            .browse => browser.keyInput(ch),
         }
         firstblock = false;
     }
diff --git a/src/model.zig b/src/model.zig
index 760300eff495d5d8d8703340f418d49c25fce50a..842e2204e209ba42faa0d946c945a0ef7ba61d16 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -1,5 +1,6 @@
 const std = @import("std");
 const main = @import("main.zig");
+const ui = @import("ui.zig");
 usingnamespace @import("util.zig");
 
 // While an arena allocator is optimimal for almost all scenarios in which ncdu
@@ -67,17 +68,23 @@ pub const Entry = packed struct {
         return @intToPtr(*Ext, std.mem.alignForward(@ptrToInt(self) + nameOffset(self.etype) + n.len + 1, @alignOf(Ext)));
     }
 
-    pub fn create(etype: EType, isext: bool, ename: []const u8) !*Entry {
+    pub fn create(etype: EType, isext: bool, ename: []const u8) *Entry {
         const base_size = nameOffset(etype) + ename.len + 1;
         const size = (if (isext) std.mem.alignForward(base_size, @alignOf(Ext))+@sizeOf(Ext) else base_size);
-        var ptr = try allocator.allocator.allocWithOptions(u8, size, @alignOf(Entry), null);
+        var ptr = blk: {
+            while (true) {
+                if (allocator.allocator.allocWithOptions(u8, size, @alignOf(Entry), null)) |p|
+                    break :blk p
+                else |_| {}
+                ui.oom();
+            }
+        };
         std.mem.set(u8, ptr, 0); // kind of ugly, but does the trick
         var e = @ptrCast(*Entry, ptr);
         e.etype = etype;
         e.isext = isext;
         var name_ptr = @intToPtr([*]u8, @ptrToInt(e) + nameOffset(etype));
         std.mem.copy(u8, name_ptr[0..ename.len], ename);
-        //std.debug.warn("{any}\n", .{ @ptrCast([*]u8, e)[0..size] });
         return e;
     }
 
@@ -95,8 +102,7 @@ pub const Entry = packed struct {
     }
 
     // Insert this entry into the tree at the given directory, updating parent sizes and item counts.
-    // (TODO: This function creates an unrecoverable mess on OOM, need to do something better)
-    pub fn insert(self: *Entry, parents: *const Parents) !void {
+    pub fn insert(self: *Entry, parents: *const Parents) void {
         self.next = parents.top().sub;
         parents.top().sub = self;
         if (self.dir()) |d| std.debug.assert(d.sub == null);
@@ -121,7 +127,7 @@ pub const Entry = packed struct {
 
             } else if (self.link()) |l| {
                 const n = HardlinkNode{ .ino = l.ino, .dir = p, .num_files = 1 };
-                var d = try devices.items[dev].hardlinks.getOrPut(n);
+                var d = devices.items[dev].hardlinks.getOrPut(n) catch unreachable;
                 new_hl = !d.found_existing;
                 if (d.found_existing) d.entry.key.num_files += 1;
                 // First time we encounter this file in this dir, count it.
@@ -285,12 +291,12 @@ const Device = struct {
 var devices: std.ArrayList(Device) = std.ArrayList(Device).init(main.allocator);
 var dev_lookup: std.AutoHashMap(u64, DevId) = std.AutoHashMap(u64, DevId).init(main.allocator);
 
-pub fn getDevId(dev: u64) !DevId {
-    var d = try dev_lookup.getOrPut(dev);
+pub fn getDevId(dev: u64) DevId {
+    var d = dev_lookup.getOrPut(dev) catch unreachable;
     if (!d.found_existing) {
         errdefer dev_lookup.removeAssertDiscard(dev);
         d.entry.value = @intCast(DevId, devices.items.len);
-        try devices.append(.{ .dev = dev });
+        devices.append(.{ .dev = dev }) catch unreachable;
     }
     return d.entry.value;
 }
@@ -308,8 +314,8 @@ pub const Parents = struct {
 
     const Self = @This();
 
-    pub fn push(self: *Self, dir: *Dir) !void {
-        return self.stack.append(dir);
+    pub fn push(self: *Self, dir: *Dir) void {
+        return self.stack.append(dir) catch unreachable;
     }
 
     // Attempting to remove the root node is considered a bug.
@@ -338,13 +344,14 @@ pub const Parents = struct {
         return .{ .lst = self };
     }
 
-    pub fn path(self: *const Self, wr: anytype) !void {
+    // Append the path to the given arraylist. The list is assumed to use main.allocator, so it can't fail.
+    pub fn path(self: *const Self, out: *std.ArrayList(u8)) void {
         const r = root.entry.name();
-        try wr.writeAll(r);
+        out.appendSlice(r) catch unreachable;
         var i: usize = 0;
         while (i < self.stack.items.len) {
-            if (i != 0 or r[r.len-1] != '/') try wr.writeByte('/');
-            try wr.writeAll(self.stack.items[i].entry.name());
+            if (i != 0 or r[r.len-1] != '/') out.append('/') catch unreachable;
+            out.appendSlice(self.stack.items[i].entry.name()) catch unreachable;
             i += 1;
         }
     }
diff --git a/src/scan.zig b/src/scan.zig
index b90b8479e9000b0c7be2a13d27950f5ef951d3d8..6323ce5d67bec1987944067a57cc949491bba80c 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -132,7 +132,7 @@ const Context = struct {
     const Self = @This();
 
     fn initFile(out: std.fs.File) !Self {
-        var buf = try main.allocator.create(Writer);
+        var buf = main.allocator.create(Writer) catch unreachable;
         errdefer main.allocator.destroy(buf);
         buf.* = std.io.bufferedWriter(out.writer());
         var wr = buf.writer();
@@ -154,13 +154,13 @@ const Context = struct {
     }
 
     // Add the name of the file/dir entry we're currently inspecting
-    fn pushPath(self: *Self, name: []const u8) !void {
-        try self.path_indices.append(self.path.items.len);
-        if (self.path.items.len > 1) try self.path.append('/');
+    fn pushPath(self: *Self, name: []const u8) void {
+        self.path_indices.append(self.path.items.len) catch unreachable;
+        if (self.path.items.len > 1) self.path.append('/') catch unreachable;
         const start = self.path.items.len;
-        try self.path.appendSlice(name);
+        self.path.appendSlice(name) catch unreachable;
 
-        try self.path.append(0);
+        self.path.append(0) catch unreachable;
         self.name = self.path.items[start..self.path.items.len-1:0];
         self.path.items.len -= 1;
     }
@@ -177,7 +177,7 @@ const Context = struct {
     }
 
     fn pathZ(self: *Self) [:0]const u8 {
-        return arrayListBufZ(&self.path) catch unreachable;
+        return arrayListBufZ(&self.path);
     }
 
     // Set a flag to indicate that there was an error listing file entries in the current directory.
@@ -195,12 +195,12 @@ const Context = struct {
 
         if (t == .err) {
             if (self.last_error) |p| main.allocator.free(p);
-            self.last_error = try main.allocator.dupeZ(u8, self.path.items);
+            self.last_error = main.allocator.dupeZ(u8, self.path.items) catch unreachable;
         }
 
         if (self.parents) |*p| {
-            var e = try model.Entry.create(.file, false, self.name);
-            e.insert(p) catch unreachable;
+            var e = model.Entry.create(.file, false, self.name);
+            e.insert(p);
             var f = e.file().?;
             switch (t) {
                 .err => e.set_err(p),
@@ -233,10 +233,10 @@ const Context = struct {
             const etype = if (self.stat.dir) model.EType.dir
                           else if (self.stat.hlinkc) model.EType.link
                           else model.EType.file;
-            var e = try model.Entry.create(etype, main.config.extended, self.name);
+            var e = model.Entry.create(etype, main.config.extended, self.name);
             e.blocks = self.stat.blocks;
             e.size = self.stat.size;
-            if (e.dir()) |d| d.dev = try model.getDevId(self.stat.dev);
+            if (e.dir()) |d| d.dev = model.getDevId(self.stat.dev);
             if (e.file()) |f| f.notreg = !self.stat.dir and !self.stat.reg;
             // TODO: Handle the scenario where we don't know the hard link count
             // (i.e. on imports from old ncdu versions that don't have the "nlink" field)
@@ -249,8 +249,8 @@ const Context = struct {
             if (self.items_seen == 0)
                 model.root = e.dir().?
             else {
-                try e.insert(p);
-                if (e.dir()) |d| try p.push(d); // Enter the directory
+                e.insert(p);
+                if (e.dir()) |d| p.push(d); // Enter the directory
             }
 
         } else if (self.wr) |wr| {
@@ -286,8 +286,7 @@ const Context = struct {
 var active_context: ?*Context = null;
 
 // Read and index entries of the given dir.
-// (TODO: shouldn't error on OOM but instead call a function that waits or something)
-fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) (std.fs.File.Writer.Error || std.mem.Allocator.Error)!void {
+fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) std.fs.File.Writer.Error!void {
     // XXX: The iterator allocates 8k+ bytes on the stack, may want to do heap allocation here?
     var it = dir.iterate();
     while(true) {
@@ -297,9 +296,9 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) (std.fs.File.Writer.Err
         } orelse break;
 
         ctx.stat.dir = false;
-        try ctx.pushPath(entry.name);
+        ctx.pushPath(entry.name);
         defer ctx.popPath();
-        try main.handleEvent(false, false);
+        main.handleEvent(false, false);
 
         // XXX: This algorithm is extremely slow, can be optimized with some clever pattern parsing.
         const excluded = blk: {
@@ -378,7 +377,7 @@ pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
 
     const full_path = std.fs.realpathAlloc(main.allocator, path) catch null;
     defer if (full_path) |p| main.allocator.free(p);
-    try ctx.pushPath(full_path orelse path);
+    ctx.pushPath(full_path orelse path);
 
     ctx.stat = try Stat.read(std.fs.cwd(), ctx.pathZ(), true);
     if (!ctx.stat.dir) return error.NotADirectory;
@@ -703,7 +702,7 @@ const Import = struct {
                 else => self.die("expected ',' or '}'"),
             }
         }
-        if (name) |n| self.ctx.pushPath(n) catch unreachable
+        if (name) |n| self.ctx.pushPath(n)
         else self.die("missing \"name\" field");
         if (special) |s| self.ctx.addSpecial(s) catch unreachable
         else self.ctx.addStat(dir_dev) catch unreachable;
@@ -733,7 +732,7 @@ const Import = struct {
         self.ctx.popPath();
 
         if ((self.ctx.items_seen & 1023) == 0)
-            main.handleEvent(false, false) catch unreachable;
+            main.handleEvent(false, false);
     }
 
     fn root(self: *Self) void {
@@ -791,7 +790,7 @@ pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) !void {
 var animation_pos: u32 = 0;
 var need_confirm_quit = false;
 
-fn drawBox() !void {
+fn drawBox() void {
     ui.init();
     const ctx = active_context.?;
     const width = saturateSub(ui.cols, 5);
@@ -808,7 +807,7 @@ fn drawBox() !void {
 
     box.move(3, 2);
     ui.addstr("Current item: ");
-    ui.addstr(try ui.shorten(try ui.toUtf8(ctx.pathZ()), saturateSub(width, 18)));
+    ui.addstr(ui.shorten(ui.toUtf8(ctx.pathZ()), saturateSub(width, 18)));
 
     if (ctx.last_error) |path| {
         box.move(5, 2);
@@ -816,7 +815,7 @@ fn drawBox() !void {
         ui.addstr("Warning: ");
         ui.style(.default);
         ui.addstr("error scanning ");
-        ui.addstr(try ui.shorten(try ui.toUtf8(path), saturateSub(width, 28)));
+        ui.addstr(ui.shorten(ui.toUtf8(path), saturateSub(width, 28)));
         box.move(6, 3);
         ui.addstr("some directory sizes may not be correct.");
     }
@@ -855,7 +854,7 @@ fn drawBox() !void {
     }
 }
 
-pub fn draw() !void {
+pub fn draw() void {
     switch (main.config.scan_ui) {
         .none => {},
         .line => {
@@ -873,11 +872,11 @@ pub fn draw() !void {
             }
             _ = std.io.getStdErr().write(line) catch {};
         },
-        .full => try drawBox(),
+        .full => drawBox(),
     }
 }
 
-pub fn keyInput(ch: i32) !void {
+pub fn keyInput(ch: i32) void {
     if (need_confirm_quit) {
         switch (ch) {
             'y', 'Y' => if (need_confirm_quit) ui.quit(),
diff --git a/src/ui.zig b/src/ui.zig
index 4a2f7610549b3bc27dc04c3e2d584a31d614c974..f5b7a0749ad575040d23b8ba63e805605cd5b790 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -29,6 +29,24 @@ pub fn quit() noreturn {
     std.process.exit(0);
 }
 
+// Should be called when malloc fails. Will show a message to the user, wait
+// for a second and return to give it another try.
+// Glitch: this function may be called while we're in the process of drawing
+// the ncurses window, in which case the deinit/reinit will cause the already
+// drawn part to be discarded. A redraw will fix that, but that tends to only
+// happen after user input.
+// Also, init() and other ncurses-related functions may have hidden allocation,
+// no clue if ncurses will consistently report OOM, but we're not handling that
+// right now.
+pub fn oom() void {
+    const haveui = inited;
+    deinit();
+    _ = std.io.getStdErr().writer().writeAll("\x1b7\x1b[JOut of memory, trying again in 1 second. Hit Ctrl-C to abort.\x1b8") catch {};
+    std.time.sleep(std.time.ns_per_s);
+    if (haveui)
+        init();
+}
+
 var to_utf8_buf = std.ArrayList(u8).init(main.allocator);
 
 fn toUtf8BadChar(ch: u8) bool {
@@ -44,7 +62,7 @@ fn toUtf8BadChar(ch: u8) bool {
 // internal buffer that will be invalidated on the next call.
 // (Doesn't check for non-printable Unicode characters)
 // (This program assumes that the console locale is UTF-8, but file names may not be)
-pub fn toUtf8(in: [:0]const u8) ![:0]const u8 {
+pub fn toUtf8(in: [:0]const u8) [:0]const u8 {
     const hasBadChar = blk: {
         for (in) |ch| if (toUtf8BadChar(ch)) break :blk true;
         break :blk false;
@@ -56,16 +74,16 @@ pub fn toUtf8(in: [:0]const u8) ![:0]const u8 {
         if (std.unicode.utf8ByteSequenceLength(in[i])) |cp_len| {
             if (!toUtf8BadChar(in[i]) and i + cp_len <= in.len) {
                 if (std.unicode.utf8Decode(in[i .. i + cp_len])) |_| {
-                    try to_utf8_buf.appendSlice(in[i .. i + cp_len]);
+                    to_utf8_buf.appendSlice(in[i .. i + cp_len]) catch unreachable;
                     i += cp_len;
                     continue;
                 } else |_| {}
             }
         } else |_| {}
-        try to_utf8_buf.writer().print("\\x{X:0>2}", .{in[i]});
+        to_utf8_buf.writer().print("\\x{X:0>2}", .{in[i]}) catch unreachable;
         i += 1;
     }
-    return try arrayListBufZ(&to_utf8_buf);
+    return arrayListBufZ(&to_utf8_buf);
 }
 
 var shorten_buf = std.ArrayList(u8).init(main.allocator);
@@ -75,7 +93,7 @@ var shorten_buf = std.ArrayList(u8).init(main.allocator);
 // Input is assumed to be valid UTF-8.
 // Return value points to the input string or to an internal buffer that is
 // invalidated on a subsequent call.
-pub fn shorten(in: [:0]const u8, max_width: u32) ![:0] const u8 {
+pub fn shorten(in: [:0]const u8, max_width: u32) [:0] const u8 {
     if (max_width < 4) return "...";
     var total_width: u32 = 0;
     var prefix_width: u32 = 0;
@@ -98,8 +116,8 @@ pub fn shorten(in: [:0]const u8, max_width: u32) ![:0] const u8 {
     if (total_width <= max_width) return in;
 
     shorten_buf.shrinkRetainingCapacity(0);
-    try shorten_buf.appendSlice(in[0..prefix_end]);
-    try shorten_buf.appendSlice("...");
+    shorten_buf.appendSlice(in[0..prefix_end]) catch unreachable;
+    shorten_buf.appendSlice("...") catch unreachable;
 
     var start_width: u32 = prefix_width;
     var start_len: u32 = prefix_end;
@@ -111,15 +129,15 @@ pub fn shorten(in: [:0]const u8, max_width: u32) ![:0] const u8 {
         start_width += cp_width;
         start_len += cp_len;
         if (total_width - start_width <= max_width - prefix_width - 3) {
-            try shorten_buf.appendSlice(in[start_len..]);
+            shorten_buf.appendSlice(in[start_len..]) catch unreachable;
             break;
         }
     }
-    return try arrayListBufZ(&shorten_buf);
+    return arrayListBufZ(&shorten_buf);
 }
 
 fn shortenTest(in: [:0]const u8, max_width: u32, out: [:0]const u8) !void {
-    try std.testing.expectEqualStrings(out, try shorten(in, max_width));
+    try std.testing.expectEqualStrings(out, shorten(in, max_width));
 }
 
 test "shorten" {
diff --git a/src/util.zig b/src/util.zig
index 9b73de57351232738841fb262e43f41415548464..277a0069c42da3402557efea34310e67859e3775 100644
--- a/src/util.zig
+++ b/src/util.zig
@@ -38,8 +38,8 @@ pub fn blocksToSize(b: u64) u64 {
 // Ensure the given arraylist buffer gets zero-terminated and returns a slice
 // into the buffer. The returned buffer is invalidated whenever the arraylist
 // is freed or written to.
-pub fn arrayListBufZ(buf: *std.ArrayList(u8)) ![:0]const u8 {
-    try buf.append(0);
+pub fn arrayListBufZ(buf: *std.ArrayList(u8)) [:0]const u8 {
+    buf.append(0) catch unreachable;
     defer buf.items.len -= 1;
     return buf.items[0..buf.items.len-1:0];
 }