Reading shape diagnostics
Harn's typechecker and runtime aim to fail loudly at the closest point to the bug. Most authoring mistakes around shapes, structs, schemas, and nilable values surface as one of the diagnostic shapes documented here.
Looking for the full list of
HARN-<CAT>-<NNN>codes, their repair ids, and safety classes? See the generated diagnostic codes catalog (or the JSON sidecar atdocs/diagnostics-catalog.json). This page stays focused on the most common shape and nilable patterns.
When you hit one of these, the message tells you the contract (what the
type declares), the observed value (what arrived), and the fix
(?., assert_shape, if x != nil, etc.) directly. You should rarely
need to dig into the source to interpret one.
Missing field on a shape annotation
type User = {name: string, email: string, age: int}
let u: User = {name: "Ada", email: "ada@x", age: 36}
log(u.emial)
error: field `emial` does not exist on shape `{name: string, email: string, age: int}` — did you mean `email`?
|
5 | log(u.emial)
= help: available fields: name, email, age
The diagnostic includes:
- The expected shape — useful when the alias resolves to an inline shape.
- A
did you meansuggestion when a field name is one or two edits away. - The full set of available fields so you don't have to chase the type alias back to its definition.
The same diagnostic fires for struct fields:
error: field `emial` does not exist on struct `User` — did you mean `email`?
= help: available fields: name, email
Property access on a nil value
A value whose static type is exactly nil cannot have fields read off it.
The error explicitly tells you the type is nil and points at the two
canonical fixes — optional access and the nil-guard:
error: cannot access property `foo` on `nil`; the value is statically known to be nil here
= help: use the optional access operator `?.foo`, or narrow the value with a `!= nil` guard before reading fields
Property access on a nilable type
A T? (i.e. T | nil) value may be nil at runtime. Direct field access
produces the same fix-suggestion as the nil case:
error: cannot access property `name` on nilable type `{name: string, email: string}?`; the value may be nil at runtime
= help: use the optional access operator `?.name`, or narrow the value with a `!= nil` guard to drop the nil arm
The canonical guard pattern, which lets the typechecker narrow the inner
shape inside the if body, is:
let data = r.data // r.data: T | nil
if data != nil {
log(data.name) // data: T here
}
Property access on an unknown value
unknown is the safe top type — its whole point is to force a narrowing
step before fields can be read. The diagnostic surfaces as a warning so
gradual code keeps running while still telling the author what to do:
warning: property access `.verdict` on an `unknown` value will fail at runtime if the value is not a shape with that field
= help: narrow with `is_a`/`type_of`, validate with `assert_shape`, or annotate with a shape type before accessing fields
The schema_is(value, Shape) form participates in flow narrowing — inside
the truthy branch, value narrows to Shape and field access is
strict-checked against it. See Schema as type.
Loose dict literals stay lenient
let d = {a: 1, b: 2} is inferred as a structural shape, but historically
this idiom also covers "I want a dynamic dict." Harn keeps it lenient:
a missing field on a literal-inferred shape returns nil at runtime
instead of erroring. To opt into strict checking, annotate the binding
or thread it through a typed function parameter:
// Lenient — d.missing returns nil
let d = {a: 1, b: 2}
// Strict — d.missing is a typecheck error
type Counts = {a: int, b: int}
let d: Counts = {a: 1, b: 2}
The same opt-in applies to var x = nil widening loops; annotating with
var x: T? = nil enables the strict diagnostics. Use the unannotated
form for genuinely dynamic loops, and the annotated form when you want
the typechecker to enforce the invariant.
Stdlib helper option errors
Stdlib collection helpers — pick_keys, filter_nil, merge, pick,
omit — declare typed signatures (dict<string, V>, typed option
shapes like PickKeysOptions = {drop_nil?: bool}). The typechecker
catches misuse statically against the declared contract:
error: function `pick_keys` parameter `d`: expected dict<string, V>, found string
For option-bag parameters, direct dict literals also reject unknown keys:
error: argument 3 `options`: unknown option `dropnil`; expected one of `drop_nil` — did you mean `drop_nil`?
When a value flows in dynamically (e.g. via unknown or a boundary
source), the runtime parameter guard catches it with the parameter name
intact:
Type error: parameter 'd': expected dict or struct, got string
Either way the failure points at the helper boundary, not several frames down inside the helper body.
Runtime shape validation
A runtime assert_shape (used by typed-parameter checks at fn and
pipeline boundaries) reports the missing field, the closest match,
and the keys actually present:
Type error: parameter 'u': missing field 'age' (int) — available fields: name, email, agee
The "available fields: …" tail is especially useful when an external
boundary (LLM output, JSON parse, host bridge call) silently returned a
near-miss of the expected shape — reading the actual keys often
diagnoses the bug without rerunning under a debugger. The wording
matches the static typechecker so authors see the same phrasing whether
the failure surfaces at harn check time or runtime shape validation.