Skip to main content
Kraken TUI uses a structured error handling system that translates FFI error codes into typed exceptions.

KrakenError Class

All errors from the native core are wrapped in KrakenError:
export class KrakenError extends Error {
  public readonly code: number;

  constructor(message: string, code: number) {
    super(message);
    this.name = "KrakenError";
    this.code = code;
  }
}

Error Codes

CodeTypeSource
-1ErrorValidation failure, invalid handle, or runtime error
-2PanicInternal panic caught at FFI boundary

Usage Example

try {
  const box = new Box({ width: "100%", height: "100%" });
  box.setStyle({ fg: "#FF0000" });
  app.setRoot(box);
} catch (error) {
  if (error instanceof KrakenError) {
    console.error(`Native error [${error.code}]: ${error.message}`);
    if (error.code === -2) {
      console.error("Fatal: internal panic occurred");
      process.exit(1);
    }
  }
}

checkResult() Helper

The checkResult() function translates FFI return codes into exceptions:
export function checkResult(code: number, context?: string): void {
  if (code >= 0) return;

  let message: string;
  if (code === -2) {
    message = "Internal panic in native core";
  } else {
    const errPtr = ffi.tui_get_last_error();
    if (errPtr) {
      message = new CString(errPtr).toString();
      ffi.tui_clear_error();
    } else {
      message = "Unknown error";
    }
  }

  if (context) {
    message = `${context}: ${message}`;
  }

  throw new KrakenError(message, code);
}

Context Parameter

Provide context to make errors more actionable:
checkResult(
  ffi.tui_set_content(handle, ptr, len),
  "Failed to set text content"
);
// Error message: "Failed to set text content: invalid handle: 42"
All Widget class methods use checkResult() internally, so you rarely need to call it directly.

Common Error Categories

Invalid Handle Errors

Occur when using a destroyed or non-existent handle:
const box = new Box();
box.destroy();
box.setStyle({ fg: "red" }); // KrakenError: invalid handle: 1
Prevention:
  • Don’t use widgets after calling .destroy()
  • Use .destroySubtree() for recursive cleanup
  • Check handle !== 0 before FFI calls

Tree Invariant Violations

Occur when violating tree structure rules:
const box1 = new Box();
const box2 = new Box();
const text = new Text({ content: "Hello" });

box1.append(text);
box2.append(text); // KrakenError: node already has a parent
Prevention:
  • Remove a child from its parent before re-parenting: parent.removeChild(child)
  • Use .destroySubtree() when unmounting component trees

Encoding Errors

Occur when passing invalid UTF-8:
const invalidUtf8 = new Uint8Array([0xFF, 0xFE]);
ffi.tui_set_content(handle, ptr(invalidUtf8), 2);
// KrakenError: invalid UTF-8
Prevention:
  • Use TextEncoder to encode strings: new TextEncoder().encode(str)
  • Validate external input before passing to FFI

Render Failures

Occur when the terminal is in an invalid state:
app.shutdown();
app.render(); // KrakenError: context not initialized
Prevention:
  • Call Kraken.init() before any other operations
  • Don’t call methods after shutdown()
  • Handle terminal resize events gracefully

Error Recovery Strategies

Graceful Degradation

function safeRender(app: Kraken): boolean {
  try {
    app.render();
    return true;
  } catch (error) {
    if (error instanceof KrakenError) {
      console.warn("Render failed, skipping frame:", error.message);
      return false;
    }
    throw error; // Re-throw non-Kraken errors
  }
}

while (running) {
  app.readInput(16);
  for (const event of app.drainEvents()) {
    handleEvent(event);
  }
  safeRender(app);
}

Widget Creation Validation

function createWidget(type: NodeType): Widget | null {
  try {
    switch (type) {
      case NodeType.Box:
        return new Box();
      case NodeType.Text:
        return new Text();
      default:
        return null;
    }
  } catch (error) {
    console.error("Widget creation failed:", error);
    return null;
  }
}

State Reset on Error

class AppState {
  private root?: Box;

  reset(): void {
    try {
      this.root?.destroySubtree();
    } catch (error) {
      console.warn("Cleanup failed:", error);
    }
    this.root = new Box({ width: "100%", height: "100%" });
  }

  recover(error: KrakenError): void {
    console.error("Fatal error, resetting state:", error.message);
    this.reset();
  }
}

Debugging Techniques

Enable Debug Mode

Enable verbose logging in the native core:
const app = Kraken.init();
ffi.tui_set_debug(1); // Enable debug logging to stderr

// Now all tree mutations, layout recomputations, and event
// processing will log structured diagnostics
Debug mode has zero overhead when disabled but generates significant stderr output when enabled. Not recommended for production.

Inspect Error Messages

Error messages include detailed context:
try {
  ffi.tui_append_child(parent, child);
} catch (error) {
  console.error(error.message);
  // "node 42 already has parent 17"
  // "invalid handle: 99"
  // "cannot append node to itself"
}

Use Performance Counters

Diagnostics counters help identify issues:
const dirtyCount = ffi.tui_get_perf_counter(5); // Dirty node count
const eventDepth = ffi.tui_get_perf_counter(3); // Event buffer depth

if (dirtyCount > 100) {
  console.warn("High dirty node count - possible layout thrashing");
}

if (eventDepth > 50) {
  console.warn("Event buffer buildup - drain loop may be too slow");
}

Check Terminal Capabilities

const caps = ffi.tui_get_capabilities();
if ((caps & 0x01) === 0) {
  console.warn("Terminal lacks truecolor support - colors will degrade");
}
if ((caps & 0x02) === 0) {
  console.warn("Terminal lacks mouse support - no click events");
}

Memory Budget Tracking

const nodeCount = ffi.tui_get_node_count();
if (nodeCount > 1000) {
  console.warn(`High widget count: ${nodeCount} nodes`);
}

Best Practices

function safeOperation<T>(op: () => T, fallback: T): T {
  try {
    return op();
  } catch (error) {
    if (error instanceof KrakenError) {
      console.error("Operation failed:", error.message);
      return fallback;
    }
    throw error;
  }
}
function setUserText(widget: Text, input: string): void {
  if (input.length > 10000) {
    throw new Error("Input too long");
  }
  try {
    widget.setContent(input);
  } catch (error) {
    console.error("Failed to set content:", error);
    widget.setContent("[Invalid input]");
  }
}
function renderFrame(app: Kraken): void {
  let tempWidget: Box | null = null;
  try {
    tempWidget = new Box();
    // ... use widget ...
    app.render();
  } finally {
    tempWidget?.destroy();
  }
}
try {
  widget.setStyle({ fg: color });
} catch (error) {
  console.error(`setStyle failed for widget ${widget.handle}:`, error);
  console.error(`  Color: ${color}`);
  console.error(`  Widget type: ${widget.type}`);
}

Build docs developers (and LLMs) love