Diagnostics

Common compiler diagnostics and how to read current 0.0.0-dev errors.

Diagnostics are part of the user experience, but exact wording and source ranges are not compatibility API while Zynx is 0.0.0-dev. Treat this page as a map of common error categories and fixes.

Error Unions Need try Or catch

Error unions are not booleans or nullable wrappers. Use try or catch before using the success value.

let name: str? = load_user()?.name;

Relevant excerpt from current compiler output:

tests/errors/nullable_error_union_optional_before_try_error.zx:14:18: error: optional member access on error union requires `try` or `catch` before member access
    |
 14 |     let name: str? = load_user()?.name;
    |                      ^^^^^^^^^^^
 15 | 	_ = name;
    |

Fix:

let name: str? = (try load_user())?.name;
let fallback: str? = (load_user() catch { _ => null })?.name;

Unsafe Operations Need Unsafe Context

Foreign calls, raw pointer dereference, volatile access, and inline asm require unsafe:

foreign "C" fn abs(value: i32) -> i32;

fn main() {
    _ = abs(-1);
}

Current compiler output:

tests/unsafe/extern_call_error.zx:4:5: error: call to foreign function requires unsafe context
    |
  3 | fn main() {
  4 |     _ = abs(-1);
    |         ^^^^^^^
  5 | }
    |

Fix:

fn magnitude(value: i32) -> i32 {
    unsafe {
        return abs(value);
    }
}

unsafe fn also requires unsafe context at the call site. It does not make the function body implicitly unsafe.

Nullable Chains Must Stay Nullable

Use ?. for every nullable hop:

let name: str? = user?.profile?.name;

a?.b.c is diagnosed when b is nullable, because plain . would require a non-null receiver.

Current compiler output:

tests/nullable/optional_chaining_mixed_error.zx:11:21: error: optional member access expects an identifier; use `?.` for each step
    |
 10 |     let a: A? = A { b: B { c: 1 } };
 11 |     let v: i32? = a?.b.c;
    |                      ^^^
 12 | }
    |

Fix:

let v: i32? = a?.b?.c;

Reference Arguments Need &

Reference parameters require explicit borrows in ordinary calls:

fn inc(a: &u32, b: const &u32) {
    *a += *b;
}

fn main() {
    let a: u32 = 0;
    let b: u32 = 10;
    inc(a, b);
}

Current compiler output:

reference argument requires an explicit `&` borrow or reference-valued expression

Fix:

inc(&a, &b);

Assert Is A Keyword Statement

Whole-condition call style is rejected:

assert(value > 0);

Current compiler output:

`assert` is a keyword; write `assert <expr>;` or `assert <expr>, <message>;`

Fix:

assert value > 0;
assert value > 0, "positive";

Ownership Errors Point At Moves

Most owned values move on assignment or call. After a move, use the new owner or clone explicitly when the type offers clone-style API:

import {
    String
} from std.string;

fn main() {
    let text = try String("hello");
    let moved = text;

    assert moved.length() == 5;
    assert text.length() == 5;
}

Current compiler output:

tests/copy/string_move_error.zx:10:8: error: use of moved value `text`
    |
  9 |     _ = moved.length();
 10 |     _ = text.length();
    |         ^^^^
 11 | }
    |

Fix: use moved, or clone before moving when independent owned storage is intended.

Main Warnings Prefer Shorthand

Program entry points should use the shorthand form when they do not need a precise library-style signature.

Current compiler warnings:

warning: explicit `-> void` on `main` is unnecessary; write `fn main()`
warning: `main` infers propagated errors; remove explicit `throws(...) -> void`

Fix:

fn main() {
    try io.stdout.write("hello\n");
}

Library functions should still use precise fallible signatures. For the program entry point, prefer bare main; the compiler accepts explicit fallible-main signatures for now but warns because the entry shorthand already infers propagated errors:

error AppError {
    Failed
}

fn start() throws(AppError) -> void {
    throw AppError.Failed;
}

Redundant Casts And Tail Returns

The compiler warns when an explicit cast repeats an already known contextual type:

let byte: u8 = 1 as u8;

Current compiler output:

warning [unnecessary_cast]: unnecessary cast to `u8`; numeric literal already has this expected type

Fix:

let byte: u8 = 1;

Keep as for computed narrowing, distinct type conversions, raw pointer casts, and generic type selection:

let low = (word & 0xff) as u8;

A final bare return; in a void-returning function also warns because fallthrough is the canonical spelling. Guard and branch return; statements remain valid.

Current compiler output:

warning [unnecessary_return]: final `return;` in void function is unnecessary

Unsupported Source Forms

Some syntax may appear in old tests, internal fixtures, or editor grammars but is not normal source language:

  • block comments /* ... */
  • dynamic runtime require(...)
  • user-defined decorators
  • public atomics
  • broad C ABI forms such as managed strings, futures, or throwing foreign functions

Relevant excerpt from current compiler output for require(...):

tests/errors/require_dynamic_type_value_error.zx:2:15: error: dynamic require is not part of the current language; use import and locked packages
    |
  2 |     const m = try require("librequire_types_mod.zxm");
    |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  3 | 	_ = m.Point;
    |

When a diagnostic points at one of these forms, check Current Language Surface before assuming the feature is meant to work.