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: false

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) -> map { $ * 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.

app::llm_stream("hello") => $s
$s -> each { $ }
$s -> map { $ }
# 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.

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.

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

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

app::llm_stream("hello") => $s
$s -> each { $ -> 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.

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 each for automatic iteration instead.

app::llm_stream("hello") => $s
$s -> each { $ -> log }

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

See Also

DocumentDescription
Error ReferenceAll error codes with causes and resolutions
TypesType rules and value semantics
Design PrinciplesWhy rill works this way
GuideBeginner-friendly introduction