How to test for errors

I am trying to test a method that should return an error. I can’t figure out how to test it properly.

    var result = Chunk.tryFrom(&allocator, &chunk_data);
    defer result.deinit(&allocator);
    try std.testing.expectError(ChunkError.ChunkCrcInvalid, result);

if i do this, the compiler complains that i call deinit on the error union. (same with errdefer)
if i remove the defer statement, compiler complains about memory leak.

the problem was in the tryFrom method. I forgot (didn’t know) to use errdefer in case the init function fails

    pub fn tryFrom(allocator: *std.mem.Allocator, bytes: []u8) ChunkError!Chunk {
        if (bytes.len < 12) {
            return ChunkError.ChunkInvalid;
        }

        const length = bytesToU32(bytes[0..4]);

        if (bytes.len != length + 12) {
            return ChunkError.ChunkDataLengthMisMatch;
        }
        const chunk_type = try ChunkType.fromBytes(bytes[4..8]);
        const crc_from_bytes = Chunk.bytesToU32(bytes[length + 8 ..]);
        var chunk = try Chunk.init(allocator, chunk_type, bytes[8 .. length + 8]);
        errdefer chunk.deinit(allocator);
        if (chunk.crc != crc_from_bytes) {
            return ChunkError.ChunkCrcInvalid;
        }
        return chunk;
    }

1 Like

That’s by design, since the function is returning an error union. The error unio doesn’t have a deinit() method defined on it, but the value it contains does.

You need to unwrap the error union first. You often do this via try <expr> or <expr1> catch <expr2>, but you want to do this conditionally here.

Notice how the if construct supports unwrapping error unions. The form for this is:

if (expr) |value| ... else |err| ...;

You can use this to do the deinit() only if there is a value there and do nothing in case an error was returned instead (or, if you want to assert more strongly that an error should be returned, the value branch can just be unreachable).

Quick demo:

$ cat test.zig
const std = @import("std");

const Error = error{FakeError};

const Thing = struct {
    pub fn deinit(_: Thing) void {
        std.debug.print("I'm running!\n", .{});
    }
};

fn thingValue() Error!Thing {
    std.debug.print(" -> returning value\n", .{});
    return .{};
}

fn thingError() Error!Thing {
    std.debug.print(" -> returning error\n", .{});
    return error.FakeError;
}

test "thing" {
    {
        const retval = thingValue();
        defer if (retval) |t| t.deinit() else |_| unreachable;
    }
    {
        const retval = thingError();
        defer if (retval) |_| unreachable else |_| std.debug.print("I'm handling an error!\n", .{});
        try std.testing.expectError(error.FakeError, retval);
    }
}

$ zig test test.zig
 -> returning value
I'm running!
 -> returning error
I'm handling an error!
All 1 tests passed.