JS-like errors in Zig

Why do you recursively create the gpa allocators?

It doesn’t look like you actually use that to check for memory leaks in a smaller scope, which is the only use-case I could imagine.
Otherwise you could just use the passed in parent_allocator.

I think it would be easier to answer, if you described what behavior you actually want from your error type, because I currently find it difficult to reverse engineer from the code that you provided, what your intent is, which parts are essential and what parts may be based on some misconception.

My personal favorite would be to simplify it even more, create an ArenaAllocator in main get the allocator from that and pass it into the error functions, than somewhere where it makes sense in the program call the reset or deinit function of the arena, freeing all the memory that was used by the errors.

const std = @import("std");
const Allocator = std.mem.Allocator;
const GeneralPurposeAllocator = std.heap.GeneralPurposeAllocator;

fn createDeepCopy(allocator:Allocator, data: anytype) !*@TypeOf(data) {
    // TODO add a compile error if data that is expected to be changing at runtime can't be copied deeply
    // because that would mean that our error data could be broken if the actual owner of the data frees it
    // or changes it
    const data_ptr = try allocator.create(@TypeOf(data));
    data_ptr.* = data;
    return data_ptr;
}

fn Error(comptime name: []const u8, Data: type, Cause: type) type {
    return struct {
        pub const is_error: bool = true;
        const Self = @This();

        // why is name a field instead of a declaration?
        // it only makes sense as field if you actually want to change the name at runtime
        // and if you want to change it then somebody needs to manage the memory if the name was created at runtime
        name: []const u8 = name,
        data: *Data,
        cause: ?*Cause,

        pub fn init(
            allocator: Allocator,
            data: Data,
            child: ?Cause,
        ) !Self {
            return Self{
                .data = try createDeepCopy(allocator, data),
                .cause = if (child) |c| try createDeepCopy(allocator, c) else null,
            };
        }

        pub fn deinit(self: *Self, allocator: Allocator) void { // pass allocator to deinit, that way we don't have to store it
            const cause_is_error = comptime switch (@typeInfo(Cause)) {
                .@"struct" =>
                    @hasDecl(Cause, "is_error") and
                    @TypeOf(Cause.is_error) == bool and
                    Cause.is_error,
                else => false,
            };

            if (cause_is_error) {
                if(self.cause) |err| { // use if to unpack optionals
                    err.deinit(allocator); // you can call methods on pointers (one level of indirection only)
                    allocator.destroy(err);
                }
            }
            allocator.destroy(self.data);
        }
    };
}

const Location = struct { line: u8, col: u8, char: u8 };
const Error1 = Error("Unexpected token", Location, void);
const Error2 = Error("Parsing failed", void, Error1);

pub fn someFunc1(allocator: Allocator) Error1 {
    return Error1.init(
        allocator,
        .{
            .line = 8,
            .col = 25,
            .char = '$',
        },
        null,
    ) catch @panic("panic 1");
}

pub fn someFunc2(allocator: Allocator) Error2 {
    const e = someFunc1(allocator);
    return Error2.init(
        allocator,
        {},
        e,
    ) catch @panic("panic 2");
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}).init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var n2 = someFunc2(allocator);
    defer n2.deinit(allocator);

    std.debug.print("{s}\n", .{n2.name});
    std.debug.print("{s}\n", .{n2.cause.?.name});
    std.debug.print("{any}\n", .{n2.cause.?.data});
}

The above is just to show that the code can look simpler and still produce the same output, with an arena it would look even simpler.

However I want to take a break here and ask why do you want to do things this way?


From my experience it is not a good idea to shoehorn a specific way we expect some other language to work, into a new language that works differently, or said more simply, what works well in Javascript, may be a poor fit in Zig.

For Zig I would suggest that you familiarize yourself with the Diagnostic pattern:

This is also an interesting related topic (I haven’t tried that approach in practice so I am not completely sure about it yet, but it looks promising):


Further, I think you have some misconceptions about how Zig works, but I am not entirely sure about what, it definitely seems like there is some confusion about allocators.

I would recommend you to go through https://ziglings.org/ from start to finish, so that you have a solid foundation for how things work, it certainly helped me grasp a lot of the details of how Zig works, quickly.

Also feel free to ask more questions or clarify your goals more, so that we can better understand how to help you.

2 Likes