Troubleshooting

Troubleshooting

No Implicit Type Coercion

rill never converts between types automatically. Operations that silently coerce in other languages produce errors in rill.

String + Number

"count: " + 5
# Error: Arithmetic requires number, got string

Fix: Use string interpolation or explicit conversion.

"count: {5}"
# Result: "count: 5"
5 -> string
# Result: "5"

Number from String

"42" -> $ + 1
# Error: Arithmetic requires number, got string

Fix: Convert with -> number.

"42" -> number -> ($ + 1)
# Result: 43

Non-numeric strings throw on conversion:

"abc" -> number
# Error: Cannot convert "abc" to number

No Truthiness

rill requires actual bool values for conditions. Empty strings, zero, and empty lists are not “falsy.”

Condition Expects Boolean

"hello" ? "yes" ! "no"
# Error: Conditional requires boolean, got string

Fix: Produce a boolean explicitly.

"hello" -> .empty -> (!$) ? "yes" ! "no"
# Result: "yes"
0 -> ($ == 0) ? "zero" ! "nonzero"
# Result: "zero"

Negation Requires Boolean

!"hello"
# Error: Negation requires boolean, got string

Fix: Negate a boolean expression.

"hello" -> .empty -> !$
# Result: true

Type-Locked Variables

Variables lock to the type of their first assignment. Reassigning a different type fails.

"hello" => $x
"world" => $x            # OK: string to string
42 => $x
# Error: cannot assign number to string variable $x

Fix: Use a new variable or convert the value.

"hello" => $x
42 -> string => $x       # OK: still a string

Missing Dict Keys

Accessing a key that does not exist throws an error. rill has no undefined or null.

[name: "alice"] => $person
$person.age
# Error: Key 'age' not found in dict

Fix: Use ?? for a default value, or .?key to check existence.

[name: "alice"] => $person
$person.age ?? 0
# Result: 0
[name: "alice"] => $person
$person.?age ? "has age" ! "no age"
# Result: "no age"

List Index Out of Bounds

["a", "b"] => $list
$list[5]
# Error: List index out of bounds

Fix: Check length before accessing.

["a", "b"] => $list
$list -> .len -> .gt(5) ? $list[5] ! "default"
# Result: "default"

Pipe Value ($) Outside Pipe

$ refers to the current pipe value. Using it outside a pipe context produces an error.

Fix: Capture with => when you need the value later.

"hello" => $greeting -> .upper
$greeting
# Result: "hello"

Empty Collection Operations

Methods like .head and .tail error on empty collections.

[] -> .head
# Error: Cannot get head of empty list

Fix: Check .empty first.

[] => $list
$list -> .empty ? "nothing" ! ($list -> .head)
# Result: "nothing"

Spread Type Mismatch

List spread requires a list operand. Dict spread requires a dict operand.

"hello" => $str
[...$str]
# Error: Spread in list literal requires list, got string

Fix: Ensure the spread operand matches the container type.

"hello" -> .split("") => $chars
[...$chars]
# Result: ["h", "e", "l", "l", "o"]

Closure Parameter Count

Calling a closure with wrong argument count produces an error.

|a, b|($a + $b) => $add
$add(1)
# Error: Expected 2 arguments, got 1

Fix: Pass the correct number of arguments, or use default parameters.

|a, b = 0|($a + $b) => $add
$add(1)
# Result: 1

Reserved Dict Keys

keys, values, and entries are reserved method names on dicts. Using them as keys produces errors.

[keys: "test"]
# Error: Reserved key 'keys'

Fix: Choose a different key name.

[key_list: "test"] => $d
$d.key_list
# Result: "test"

Debugging Tips

Use log for Pipeline Inspection

log prints its input and passes the value through unchanged, so it works inline.

"hello" -> log -> .upper -> log -> .len
# Logs: "hello"
# Logs: "HELLO"
# Result: 5

Use json to Inspect Structure

[name: "alice", scores: [90, 85, 92]] -> json -> log
# Logs: {"name":"alice","scores":[90,85,92]}

Use ^type to Check Types

[1, 2, 3] => $val
$val.^type.name -> log
# Logs: "list"
$val.^type.signature -> log
# Logs: "list(number)"

Use Type Assertions to Validate

Insert :type assertions at pipe boundaries to catch unexpected types early.

[1, 2, 3] -> :list(number) -> fan({ $ * 2 })
# Result: [2, 4, 6]

Stream Pitfalls

Re-Iterating a Consumed Stream

A stream can be iterated only once. Passing it to a second collection operator halts execution.

use<ext:app> => $app
$app.llm_stream("hello") => $s
$s -> seq({ $ })
$s -> fan({ $ })
# Error: RILL-R002: Stream already consumed; cannot re-iterate

Fix: Consume the stream once and store results in a variable if you need the data again.

use<ext:app> => $app
$app.llm_stream("hello") => $s
$s -> fold("", { $@ ++ $ }) => $full_text
$full_text -> log
$full_text -> .len -> log

yield Outside a Stream Closure

yield is a keyword scoped to stream closure bodies. Using it outside that context is a parse error.

"hello" -> yield
# Error: RILL-P: yield is not valid outside a stream closure body

yield is also invalid inside a stored closure defined within a stream body.

|| {
  { $ -> yield } => $fn
  $fn(1)
}:stream(number):number
# Error: yield is not valid in stored closure

Fix: Use yield only as a terminator in a pipe chain inside the stream closure body directly.

|| {
  "first" -> yield
  "second" -> yield
  return 2
}:stream(string):number => $producer

Calling $s() Before Consuming the Stream

Calling $s() on a stream that has not been fully iterated triggers internal consumption. All chunks are consumed before the resolution value is returned. This prevents separate chunk processing afterward.

use<ext:app> => $app
$app.llm_stream("hello") => $s
$s()    # forces internal consumption of all chunks
$s -> seq({ $ -> log })
# Error: RILL-R002: Stream already consumed; cannot re-iterate

Fix: Iterate chunks first, then call $s() for the resolution value.

use<ext:app> => $app
$app.llm_stream("hello") => $s
$s -> seq({ $ -> log })
$s()    # safe: stream is closed, resolution is cached

Stale Step Access with .next()

Manual stream iteration with .next() creates new step objects. Holding a reference to an old step and calling .next() on it halts execution.

use<ext:app> => $app
$app.llm_stream("hello") => $s
$s.next() => $step1
$step1.next() => $step2
$step1.next()
# Error: RILL-R002: Stale step; this step is no longer current

Fix: Always reassign the step variable when advancing. Use seq for automatic iteration instead.

use<ext:app> => $app
$app.llm_stream("hello") => $s
$s -> seq({ $ -> log })

$s() remains valid on stale steps. Only .next() fails when called on a non-current step.

My Script Halted at an Access

An access on an invalid value halts execution. Common causes: a host function returned an error, a type assertion failed, or a field did not exist.

Symptom: Script stops mid-execution with no apparent syntax error.

Fix: Wrap the risky access in guard to catch the halt and inspect the result.

"hello" => $val
guard { $val.upper } => $out
$out.! ? "halted: {$out.!message}" ! $out
# Result: "HELLO"

To find which operation halted, read .!trace:

guard { app::fetch("https://api.example.com") } => $result
$result.!trace -> seq({ log("{$.kind} at {$.site}") })

Access halts are catchable. Halts from error "..." and assert are non-catchable and propagate through guard.

Why Does #MY_CODE Not Match?

Atom comparison uses identity, not string equality. An atom name that was not registered resolves to #R001.

# Error: #MY_CODE resolves to #R001 if not registered
$result.!code == #MY_CODE ? "matched" ! "no match"

Cause: #MY_CODE was not registered before the script ran.

Fix: Use a pre-registered atom, or register the atom via ctx.registerErrorCode("MY_CODE", "generic") in your host before running the script.

Use .! to test validity without comparing atoms:

"hello" => $val
guard { $val.upper } => $result
$result.! ? "invalid" ! "valid"
# Result: "valid"

Pre-registered atoms: #TIMEOUT, #AUTH, #FORBIDDEN, #RATE_LIMIT, #QUOTA_EXCEEDED, #UNAVAILABLE, #NOT_FOUND, #CONFLICT, #INVALID_INPUT, #PROTOCOL, #DISPOSED, #TYPE_MISMATCH, #R001, #R999. Note: #ok is a runtime sentinel, not a script-level atom literal — the lexer does not emit it.

To convert a registered atom to its string name:

#TIMEOUT -> string
# Result: TIMEOUT

Guard Did Not Catch My Error

guard catches catchable halts only. Halts from error "..." and assert are non-catchable.

# Non-catchable halt — propagates through guard
guard { error "fatal" }
# Error: non-catchable halt from 'error' propagates

Cause 1: The halt originated from error "..." or assert. These are intentional escalations, not recoverable failures.

Cause 2: A filtered guard<on: list[#CODE]> did not match the actual error code. Non-matching codes propagate.

guard<on: list[#TIMEOUT]> {
  app::fetch("https://api.example.com")
  # If this returns #AUTH, the halt propagates — not caught
}

Fix for cause 1: Remove guard — the script must stop. If the error is expected, do not use error "..." to produce it. Use ctx.invalidate from the host instead.

Fix for cause 2: Widen the filter or remove it to catch all catchable codes.

"hello" => $val
guard { $val.upper } => $out
$out.!
# Result: false

The filter <on: list[...]> is optional. Without it, guard catches every catchable halt.

See Also

DocumentDescription
Error ReferenceAll error codes with causes and resolutions
Error Handlingguard, retry, .!, and status probes
TypesType rules and value semantics
Design PrinciplesWhy rill works this way
GuideBeginner-friendly introduction