Absurdly long error return traces

So I wrote a parser for CSS stylesheets, and as part of its design I used Zig errors as a form of control flow. I thought it would be fine but there’s a problem: it causes the error return trace to grow extremely large (seemingly without bound). Now if the program exits with an error, even after parsing, I get hit with a wall of useless error information, and I’m unable to see the actually important error.

Example error return trace
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        return error.ControlFlowSuspend;
        ^
C:\zss\source\syntax\parse.zig:360:9: 0x7ff6c2d463fa in pushQualifiedRule (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .qualified_rule = .{ .is_style_rule = is_style_rule } } });
        ^
C:\zss\source\syntax\parse.zig:459:17: 0x7ff6c2d4688d in consumeListOfRules (zss-unit-tests.exe.obj)
                return parser.pushQualifiedRule(ast, saved_location, data.top_level);
                ^
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        return error.ControlFlowSuspend;
        ^
C:\zss\source\syntax\parse.zig:378:9: 0x7ff6c2d46c39 in pushStyleBlock (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .style_block = .{} } });
        ^
C:\zss\source\syntax\parse.zig:518:29: 0x7ff6c2d46ef7 in consumeQualifiedRule (zss-unit-tests.exe.obj)
                    true => try parser.pushStyleBlock(ast, saved_location),
                            ^
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        return error.ControlFlowSuspend;
        ^
C:\zss\source\syntax\parse.zig:396:9: 0x7ff6c2d4792f in pushDeclarationValue (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .declaration_value = .{} } });
        ^
C:\zss\source\syntax\parse.zig:598:17: 0x7ff6c2d47bbd in consumeDeclarationStart (zss-unit-tests.exe.obj)
                try parser.pushDeclarationValue(ast, name_location, style_block, previous_declaration);
                ^
C:\zss\source\syntax\parse.zig:538:29: 0x7ff6c2d486aa in consumeStyleBlockContents (zss-unit-tests.exe.obj)
            .token_ident => try consumeDeclarationStart(parser, location, ast, data, saved_location, data.index_of_last_declaration),
                            ^
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        return error.ControlFlowSuspend;
        ^
C:\zss\source\syntax\parse.zig:396:9: 0x7ff6c2d4792f in pushDeclarationValue (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .declaration_value = .{} } });
        ^
C:\zss\source\syntax\parse.zig:598:17: 0x7ff6c2d47bbd in consumeDeclarationStart (zss-unit-tests.exe.obj)
                try parser.pushDeclarationValue(ast, name_location, style_block, previous_declaration);
                ^
C:\zss\source\syntax\parse.zig:538:29: 0x7ff6c2d486aa in consumeStyleBlockContents (zss-unit-tests.exe.obj)
            .token_ident => try consumeDeclarationStart(parser, location, ast, data, saved_location, data.index_of_last_declaration),
                            ^
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        return error.ControlFlowSuspend;
        ^
C:\zss\source\syntax\parse.zig:396:9: 0x7ff6c2d4792f in pushDeclarationValue (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .declaration_value = .{} } });
        ^
C:\zss\source\syntax\parse.zig:598:17: 0x7ff6c2d47bbd in consumeDeclarationStart (zss-unit-tests.exe.obj)
                try parser.pushDeclarationValue(ast, name_location, style_block, previous_declaration);
                ^
C:\zss\source\syntax\parse.zig:538:29: 0x7ff6c2d486aa in consumeStyleBlockContents (zss-unit-tests.exe.obj)
            .token_ident => try consumeDeclarationStart(parser, location, ast, data, saved_location, data.index_of_last_declaration),
                            ^
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        return error.ControlFlowSuspend;
        ^
C:\zss\source\syntax\parse.zig:396:9: 0x7ff6c2d4792f in pushDeclarationValue (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .declaration_value = .{} } });
        ^
C:\zss\source\syntax\parse.zig:598:17: 0x7ff6c2d47bbd in consumeDeclarationStart (zss-unit-tests.exe.obj)
                try parser.pushDeclarationValue(ast, name_location, style_block, previous_declaration);
                ^
C:\zss\source\syntax\parse.zig:538:29: 0x7ff6c2d486aa in consumeStyleBlockContents (zss-unit-tests.exe.obj)
            .token_ident => try consumeDeclarationStart(parser, location, ast, data, saved_location, data.index_of_last_declaration),
                            ^
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        return error.ControlFlowSuspend;
C:\zss\source\syntax\parse.zig:396:9: 0x7ff6c2d4792f in pushDeclarationValue (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .declaration_value = .{} } });
        ^
C:\zss\source\syntax\parse.zig:598:17: 0x7ff6c2d47bbd in consumeDeclarationStart (zss-unit-tests.exe.obj)
                try parser.pushDeclarationValue(ast, name_location, style_block, previous_declaration);
C:\zss\source\syntax\parse.zig:538:29: 0x7ff6c2d486aa in consumeStyleBlockContents (zss-unit-tests.exe.obj)
            .token_ident => try consumeDeclarationStart(parser, location, ast, data, saved_location, data.index_of_last_declaration),
                            ^
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        ^
C:\zss\source\syntax\parse.zig:396:9: 0x7ff6c2d4792f in pushDeclarationValue (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .declaration_value = .{} } });
        ^
                try parser.pushDeclarationValue(ast, name_location, style_block, previous_declaration);
                ^
C:\zss\source\syntax\parse.zig:538:29: 0x7ff6c2d486aa in consumeStyleBlockContents (zss-unit-tests.exe.obj)
            .token_ident => try consumeDeclarationStart(parser, location, ast, data, saved_location, data.index_of_last_declaration),
C:\zss\source\syntax\parse.zig:305:9: 0x7ff6c2d462a7 in pushFrame (zss-unit-tests.exe.obj)
        return error.ControlFlowSuspend;
        ^
C:\zss\source\syntax\parse.zig:396:9: 0x7ff6c2d4792f in pushDeclarationValue (zss-unit-tests.exe.obj)
        try parser.pushFrame(.{ .index = index, .data = .{ .declaration_value = .{} } });
        ^
(126 additional stack frames skipped...)

Anyone know how to get rid of this large output? Is there a way to “trim/clear” the error return trace? Or should I just stop using errors as control flow?

From a brief glance through your linked code, you’re not handling the error you’re returning, so as soon as you call pushFrame() you’ll get a complete stack trace of how you got to that function. If you have any work going on in parallel, you’ll potentially get as many errors stacks as you have threads, depending on when and where you’re handling the error.

What are you hoping to see?

The error is being caught in the loop function.

To be more clear, the situation I’m in looks like this:

fn doStuff() void {
    try runParser();
    ....

    // much later...
    return error.Oops;
}

I’m hoping to see a relatively small error trace that just shows error.Oops, but instead all the control flow from parsing is somehow retained, and I get a super long trace.

I wonder whether putting this part

into its own function and calling it would help with basically trimming the error stack trace to only include things from the current iteration of the while loop, but I am not sure whether that would work.

I think if I was writing this code I would want to see whether this while(true) { ... } loop could be refactored to instead become a labeled switch.


Basically I am wondering whether the parsing logic would be easier to understand, if it worked fully iteratively instead of using recursion, keeping your own stack and context. But I don’t really know, I would have to invest too much time to look at the control flow in much more detail.


Considering that most of your functions return void, you could return an enum instead, that signals whether the top frame was pushed (it seems that is what you are signaling with that error) or whether parsing can continue normally.

So basically you would be forced to handle that signal more explicitly instead of bubbling it up implicitly.

1 Like

I was also thinking I could rewrite it to use an enum. It would just suck for code quality because errors are so much easier to use.

Your suggestion of putting it into its own function worked! (even though I don’t understand why…) The error return trace is now readable. Here’s how I changed it:

fn loop(parser: *Parser, location: *TokenSource.Location, ast: *AstManaged) !void {
    while (parser.stack.top) |*frame| {
        loopInner(parser, location, ast, frame) catch |err| switch (err) {
            error.ControlFlowSuspend => {},
            else => |e| return e,
        };
    }
}

fn loopInner(parser: *Parser, location: *TokenSource.Location, ast: *AstManaged, frame: *Parser.Frame) !void {
    // zig fmt: off
    switch (frame.data) {
        .list_of_rules            =>     |*list_of_rules| try consumeListOfRules(parser, location, ast, list_of_rules),
        .list_of_component_values =>                      try consumeListOfComponentValues(parser, location, ast),
        .qualified_rule           =>    |*qualified_rule| try consumeQualifiedRule(parser, location, ast, qualified_rule),
        .at_rule                  =>           |*at_rule| try consumeAtRule(parser, location, ast, at_rule),
        .style_block              =>       |*style_block| try consumeStyleBlockContents(parser, location, ast, style_block),
        .declaration_value        => |*declaration_value| try consumeDeclarationValue(parser, location, ast, declaration_value),
        .simple_block             =>      |*simple_block| try consumeSimpleBlock(parser, location, ast, simple_block),
    }
    // zig fmt: on
}

1 Like

I also don’t understand fully why, it was just from vague intuition that I suspected it could work. I guess I would have to study the compiler implementation a bit more to fully explain it.

Or maybe somebody who already knows more about how error return traces work internally can explain.