Troubleshooting
From build errors to state lifetime to rendering glitches: the shortest path to the cause.
Build errors
invalid fingerprint
A template’s fingerprint can’t be reused. Delete the .fingerprint line in build.zig.zon, run zig build once, and paste the unique value Zig prints back in.
import of file outside module path
Don’t run zig test on framework-internal files directly. Use the repo’s test steps such as zig build test-ui (see Commands).
@import("ui") / @import("zenit_app") not found
Check that build.zig.zon declares the .zenit dependency and that you call zenit.attach(zenit_dep, exe) after creating the executable. It adds the ui / zenit_app modules, compiles the macOS bridges and links the system frameworks.
build API field mismatch
Run zig version first. zenit requires Zig 0.15.2 (minimum_zig_version in build.zig.zon); other versions’ build APIs aren’t supported.
-Dtest-mode=true has no effect
Zig isolates dependency options. Forward .@"test-mode" and .@"e2e-port" explicitly in b.dependency("zenit", ...), as templates/minimal-app/build.zig does.
Runtime and state
error.StateNotFound
An explicit-id cx.handler(State, id, method) ran before that state existed. Prefer cx.bindState + cx.on: they pass the state pointer directly, so there are no ids to manage.
leaked ArrayList / HashMap at exit
State from bindState is owned by the Cx and freed when the Cx is; if T declares a pub deinit(self: *T), the framework calls it automatically. A leak usually means deinit isn’t pub, or the resource should have followed a page’s lifetime. See the next section.
crash after switching pages
Look for a previous page’s *Node or *Scope kept in global state. Once the page’s Scope is disposed, those pointers are dead and must be cleared with it.
Invalid free after updating text
The node’s current text may be a copy the node owns. Don’t edit content on props from getText() and setText them back; use try node.setTextContent(cx.allocator, "Updated"), which copies the string and marks ownership correctly.
Cleaning up state correctly
Keep an allocator field in the state and let a pub deinit do the cleanup. The initial value must not own resources yet. Allocate after bindState.
const Editor = struct {
allocator: std.mem.Allocator,
lines: std.ArrayList([]const u8) = .empty,
// pub: the Cx calls this automatically when it frees the state.
pub fn deinit(self: *Editor) void {
self.lines.deinit(self.allocator);
}
};
// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(Editor, .{ .allocator = cx.allocator });Reach for scope.onCleanup only when the cleanup isn’t a pub deinit, or when the resource must follow a Scope (a page) rather than the whole Cx.
// Resources that must follow a page (Scope), not the whole window (Cx).
const PageCache = struct {
allocator: std.mem.Allocator,
map: std.StringHashMapUnmanaged(u32) = .empty,
// Not named `pub fn deinit`: the Cx would call it again and double-free.
fn release(self: *PageCache) void {
self.map.deinit(self.allocator);
}
};
const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);Layout and drawing
state changed but the text did not
setText already compares old and new signatures and marks sizing / render dirty as needed, so no manual markRenderDirty is required. Stale text usually means a TextProps copy was edited without calling setText, or the displayed value never subscribed to its Signal. Prefer ui.textFmt(cx, scope, fmt, .{ signals }, props) to subscribe to Signals / Memos directly.
some colors ignore a theme switch
boxStyled / textStyled and friends attach an on_theme hook, and cx.setTheme replays those only across the cx.root subtree; plain nodes and component styles are snapshots taken at mount. Rebuild those trees on a theme change, or subscribe to cx.themeSignal(scope) in an Effect and restyle; nodes not attached under cx.root aren’t updated either.
clicks fall through an overlay’s empty area
Purely visual containers with no interaction default to pass_through. Set hit_behavior = .@"opaque" on the overlay root’s style extension, or use Modal / Sheet, which bring their own barrier.
a huge invisible hit area in the window
Attach ui.devtools.overlay to inspect the node being hit, and print its screen rect with node.globalRect(). The usual cause is a parent’s fill / grow sizing or an oversized absolute position; for hidden overlays, check that they are removed from hit-testing: a subtree hidden with node.setDisplay(.none) leaves layout, painting, hit-testing and Tab traversal as a whole.
// Plain visual containers are pass-through by default.
// Make an overlay root swallow clicks on its empty area:
(try panel.style.ensureExtFallible(cx.allocator)).hit_behavior = .@"opaque";Still stuck
When you file an issue, include at least:
- ✓
Versions: the zenit revision,
zig versionand your macOS version. - ✓
Reproduction: a minimal repro and the build step you ran.
- ✓
Output: the complete error output, not just the last line.
- ✓
Rendering issues: a screenshot plus the relevant nodes’ sizes from DevTools.