Improving Your Zig Language Server Experience

@kristoff new blog post Improving Your Zig Language Server Experience is a follow up to this discovery Why `-fno-emit-bin` is so much faster to get to the first compilation error?


That discussion was definitely where I first learned of the main trick (building with fno-emit-bin) but the ZLS option to run a build step on save (+ error parsing and reporting as a diagnostic) predates it (the setting was added in august 2023), which I imagine was added to support the general use case of displaying build errors in the editor.


The credit for the check discovery goes to @Brysen, who asked a very reasonable question of why zig build check isn’t a thing!


This is a great article! Can we expect to have this functionality bundled?

Thanks @kristoff for that informative article. I hope this doesn’t mean what it seems to mean:

the late Alex Naskos

My only gripe is that this, as written, assumes that there’s a check step in every project you run zls against, right?

I wonder if I could configure eglot to pass --config-path

Great article, thank you, and it helps clarify some build.zig stuff for me too :slight_smile:

I don’t think so, defining what your build looks like is always entirely up to you (the user). Maybe there might be some things that could be done to make it easier to either discover this functionality (eg have zig init create a check step), or exploit it (eg make it so your default step could be made non-emitting via a cli flag), but at it’s core the build script is userland code that you’re expected to be in control of.

related issues:

1 Like

Yes but if the step is not present nothing bad happens. See also the ZLS issue I linked in my previous message for potential future improvements.

1 Like

Alex passed away in September 2023

Deeply saddened to hear this.

Wow. Terrible news. He will be missed.

Great tip in that article. It got me thinking and I think that you can reduce that addition in build.zig to just these 2 lines right?

const check = b.step("check", "Check if foo compiles");

or just 1:

b.step("check", "Check if foo compiles").dependOn(&exe.step);

no that won’t get you the -fno-emit-bin flag, see the issues I linked above. currently you need to define a different exe step that doesn’t get referenced by b.installArtifact.

you can test this claim by doing zig build --verbose 2>&1 | grep fno-emit-bin using either configuration

1 Like

This is really neat!

I have a question. I’ve tested this out on just a basic zig init project to make sure my config was set up right and I’ve noticed that only some compile errors are emitted. For example:

const std = @import("std");

pub fn main() !void {
    // Prints to stderr (it's a shortcut based on ``)
    std.debug.print("All your {b} are belong to us.\n", .{4.01});

    var res = [_]i8{255};
    _ = &res;

    // stdout is for the actual output of your application, for example if you
    // are implementing gzip, then only the compressed bytes should be sent to
    // stdout, not any debugging messages.
    const stdout_file =;
    var bw =;
    const stdout = bw.writer();

    try stdout.print("Run `zig build test` to run the tests.\n", .{});

    try bw.flush(); // don't forget to flush!

The error with using an i8 rather than a u8 shows up, but there is no error diagnostic for using b with a float in the std.debug.print call. Both of these issue compile errors when I run my check command, so I’m curious as to why only the first one is reported by zls. Any ideas?

That’ll be because the error there isn’t being triggered at the std.debug.print call, but rather in the standard library. That std.debug.print call just happens to be in the (hypothetical) call stack for the error!

1 Like

Ahh. That makes sense. I’m trying to think about how you could trace the error back to the “correct” call-site. Not even sure how to define what the “correct” call-site would be.