---
title: "Troubleshooting | zenit Zig UI Docs"
description: "From build errors to state lifetime to rendering glitches: the shortest path to the cause."
url: https://ui.surf/zenit/docs/reference/troubleshooting
language: en
alternate_zh: https://ui.surf/zenit/zh/docs/reference/troubleshooting.md
alternate_es: https://ui.surf/zenit/es/docs/reference/troubleshooting.md
alternate_ja: https://ui.surf/zenit/ja/docs/reference/troubleshooting.md
alternate_ko: https://ui.surf/zenit/ko/docs/reference/troubleshooting.md
alternate_fr: https://ui.surf/zenit/fr/docs/reference/troubleshooting.md
alternate_de: https://ui.surf/zenit/de/docs/reference/troubleshooting.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 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](https://ui.surf/zenit/docs/reference/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`.

`state_deinit.zig`

```zig
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.

`scope_cleanup.zig`

```zig
// 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);
```

> WARNING
> 
> **Don’t register deinit twice.** If T already has a pub `deinit`, registering the same function with `scope.onCleanup` double-frees: once when the Scope is disposed and again when the Cx frees the state.

## 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](https://ui.surf/zenit/components/modal) / [Sheet](https://ui.surf/zenit/components/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.

`opaque_overlay.zig`

```zig
// 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 version` and 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.
