Skip to content

Errors

This page lists every diagnostic code Rad can emit, with examples and fixes. You can also read any entry from the terminal with rad docs <code>, e.g. rad docs RAD10001.

Codes are grouped into bands by the phase that emits them. Retired codes stay listed: numbers are never reused, so old logs remain greppable.

Syntax Errors (RAD1xxxx)

RAD10001: Invalid Syntax

This is a catch-all for syntax that doesn't match any expected pattern. The error message describes the specific problem.

Example

x = 5

// Wrong - can't assign to an expression
x + 1 = 6

// Correct
y = x + 1

How to Fix

The caret (^) in the error message points to where the parser got confused. Check for typos, missing punctuation, or mismatched brackets around that location.

If you think this error should have a more specific code, consider filing an issue.

RAD10002: Missing Colon

A block-opening keyword (if, elif, else, for, while, fn, switch, case, defer) appeared without the trailing : that introduces its body.

Example

// Wrong - the 'if' header has no colon:
if true
    print("hello")

Fix

Add the missing : at the end of the header line:

if true:
    print("hello")

Every block-opening keyword needs the colon. Rad uses indentation for block structure, but the colon is what tells the parser "a block starts on the next line."

RAD10003: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10003 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10004: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10004 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10005: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10005 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10006: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10006 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10007: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10007 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10008: Reserved Keyword

A reserved keyword was used where an identifier was expected.

Example

// Wrong
args = 5

// Correct
arguments = 5

Currently args is reserved for declaring command-line arguments. Choose a different name for your variable.

RAD10009: Unexpected Token

The parser found a token that doesn't make sense in the current context.

Example

// Wrong
x = + 5

// Correct
x = 5

The caret (^) in the error message points to the unexpected token. Look for extra symbols, misplaced keywords, or copy-paste errors around that location.

RAD10010: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10010 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10011: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10011 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10012: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10012 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10013: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10013 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10014: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10014 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10015: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10015 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10016: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10016 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10017: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10017 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10018: Missing Indent

A block-opening header (if x:, for ... :, fn name(...):) appeared but the next line wasn't indented relative to the header. Rad uses indentation for block structure - the colon promises a body, and the body has to live one indent level deeper.

Example

// Wrong - the 'if' body is at the same indent as the header:
if true:
y = 5

Fix

Indent the body. Convention is 4 spaces:

if true:
    y = 5

Tabs and spaces both work, but stay consistent within a file - mixing them confuses the indentation tracker.

RAD10019: Retired

This error code is no longer in use. Tree-sitter's error recovery produces ERROR nodes for the shape this code was designed to catch, so the dispatch in error_messages.go now falls back to RAD10001 ("Invalid syntax") or RAD10009 ("Unexpected token") instead.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD10019 remain greppable.

See rad docs RAD10001 or rad docs RAD10009 for the modern equivalents.

RAD10020: Unterminated String

A string literal opened with a quote but reached end-of-line (or end-of-file) without a matching closing quote.

Example

// Wrong - no closing quote:
message = "Hello, world

Fix

Close the string with the same quote character it opened with:

message = "Hello, world"

Rad supports double quotes ("), single quotes ('), and backticks (`) - the close quote has to match the open. For strings that genuinely span multiple lines, use backticks: they're the multi-line string form and don't trip this check.

RAD10021: Missing Operator

Two values appear next to each other without an operator between them.

Example

// Wrong
result = 5 3

// Correct
result = 5 + 3

Add an operator (+, -, *, /, ==, and, or, etc.) between the values.

RAD10022: Keyword Misuse

A keyword was used in a context where it doesn't belong.

Example

// Wrong
else:
    print("error")

// Correct
if condition:
    print("if branch")
else:
    print("else branch")

Keywords like else and elif must follow an if, case and default must be inside a switch, and break and continue must be inside a loop.

Runtime Errors (RAD2xxxx)

RAD20000: Generic Runtime Error

This is a catch-all for runtime errors that don't have a more specific error code. The error message itself describes what went wrong.

Read the error message carefully. If this error seems like it should have its own code, consider filing an issue at https://github.com/amterp/rad/issues

RAD20001: Parse Int Failed

parse_int() couldn't convert the value to an integer.

Example

// These fail
parse_int("hello")   // Not a number
parse_int("12.5")    // Decimals not allowed
parse_int("")        // Empty string

// These work
parse_int("42")      // 42
parse_int("-10")     // -10

How to Fix

Handle the error or provide a fallback:

age = parse_int(input) ?? 0

// Or with logging
age = parse_int(input) catch:
    print_err("Invalid number: {age}")
    exit(1)

If your input might have decimals, parse as float first:

x = int(parse_float("12.5") ?? 0)  // 12

RAD20002: Parse Float Failed

parse_float() couldn't convert the value to a floating-point number.

Example

// These fail
parse_float("hello")   // Not a number
parse_float("")        // Empty string
parse_float("1.2.3")   // Multiple decimal points

// These work
parse_float("3.14")    // 3.14
parse_float("42")      // 42.0
parse_float("1e10")    // Scientific notation

How to Fix

Handle the error or provide a fallback:

value = parse_float(input) ?? 0.0

// Or with logging
value = parse_float(input) catch:
    print_err("Invalid number: {value}")
    exit(1)

RAD20003: File Read Error

Rad couldn't read the file.

Common Causes

  • File doesn't exist - check the path
  • Permission denied - you don't have read access
  • Path is a directory - you tried to read a directory as a file

Example

content = read_file("/path/to/file.txt") catch:
    print_err("Could not read file: {content}")
    exit(1)

How to Fix

Check if the file exists before reading:

info = get_path("/path/to/file.txt")
if info.exists:
    content = read_file("/path/to/file.txt")

Or handle the error with a fallback:

path = "/path/to/file.txt"
content = read_file(path) ?? ""

RAD20004: File Permission Denied

You don't have permission to access this file or directory.

Example

// Reading a protected file
content = read_file("/etc/shadow")

// Writing to a protected location
write_file("/usr/bin/myfile", "content")

How to Fix

  1. Check permissions - use ls -la to see file permissions
  2. Use a different location - write to a directory you control
  3. Handle the error:
path = "/etc/shadow"
content = read_file(path) catch:
    print_err("Cannot read file: {content}")
    exit(1)

RAD20005: File Does Not Exist

The file or directory wasn't found at the specified path.

Example

content = read_file("missing.txt")  // File doesn't exist

How to Fix

Check file existence first:

filepath = "missing.txt"
info = get_path(filepath)
if info.exists:
    content = read_file(filepath)
else:
    print("File not found: {filepath}")

Or handle the error:

filepath = "missing.txt"
content = read_file(filepath) catch:
    print_err("Could not read: {content}")
    exit(1)

Common issues: typos in the path, relative paths resolving to the wrong location, or case sensitivity on some filesystems.

RAD20006: File Write Error

Rad couldn't write to the file.

Common Causes

  • Permission denied - no write access to file or directory
  • Disk full - no space left on device
  • Directory doesn't exist - parent directory is missing

How to Fix

filepath = "out.txt"
content = "hello"
write_file(filepath, content) catch:
    print_err("Failed to write: {filepath}")
    exit(1)

Check that: 1. You have write access to the directory 2. The parent directory exists 3. There's enough disk space

RAD20007: Ambiguous Epoch

The timestamp value is ambiguous - Rad can't tell if it's seconds or milliseconds since epoch.

What This Means

Unix timestamps come in two common formats: - Seconds (10 digits): 1700000000 - Milliseconds (13 digits): 1700000000000

Some values fall in an ambiguous range where they could reasonably be either.

How to Fix

Use a timestamp with a clear magnitude, or use explicit parsing that specifies the unit. Timestamps around 10 digits are typically seconds; around 13 digits are typically milliseconds.

RAD20008: Invalid Time Unit

The time-unit string passed to a time builtin isn't recognized.

Example

// Wrong - 'milliseconds' was renamed in v0.9
a = parse_epoch(1712345678000, unit="milliseconds")

// Correct
a = parse_epoch(1712345678000, unit="millis")

Valid Units

parse_epoch accepts:

  • "auto" - the default, auto-detects based on magnitude
  • "seconds"
  • "millis"
  • "micros"
  • "nanos"

The longer aliases (milliseconds, microseconds, nanoseconds) were removed in v0.9. See the v0.9 migration guide.

RAD20009: Invalid Timezone

The timezone string isn't recognized.

Example

// Wrong - abbreviations aren't reliable
t = now(tz="PST")

// Correct - use IANA timezone names
t = now(tz="America/Los_Angeles")
t = now(tz="Europe/London")
t = now(tz="UTC")

Valid Timezones

Use IANA timezone database names (format: Continent/City):

  • UTC
  • America/New_York
  • America/Los_Angeles
  • Europe/London
  • Asia/Tokyo

Full list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

RAD20010: User Input Error

An error occurred while reading user input from an interactive prompt - such as input(), confirm(), or the confirmation prompt shown before a shell command (confirm $\...`or--confirm-shell`).

This usually happens in one of two ways:

  • stdin isn't available - for example, when running a script in a non-interactive context like a cron job or piped command.
  • You aborted the prompt - pressing Ctrl-C or Esc at an interactive prompt cancels it and raises this error. (This is distinct from declining a shell confirmation by answering "n", which simply doesn't run the command.)

How to Fix

Check if interactive input is available:

if has_stdin():
    data = read_stdin()
else:
    print("No input available")

Or handle the error:

name = input("Enter name: ") catch:
    name = "default"

RAD20011: Parse JSON Failed

parse_json() couldn't parse the input as valid JSON.

Example

// These fail (raw strings so {} aren't interpreted as interpolation)
parse_json(r"{invalid}")         // Missing quotes around key
parse_json(r"{'key': 'value'}")  // Single quotes not valid
parse_json(r"{key: value}")      // Unquoted strings

// These work
parse_json(r'{"key": "value"}')
parse_json('[1, 2, 3]')
parse_json('null')

Common Issues

  • JSON requires double quotes, not single quotes
  • Object keys must be quoted strings
  • No trailing commas allowed
  • No comments in JSON

How to Fix

text = r'{"key": "value"}'
data = parse_json(text) catch:
    print_err("Invalid JSON: {data}")
    exit(1)

RAD20012: Internal Type Check Bug

This is a bug in Rad itself, not your script.

What to Do

Please report this at https://github.com/amterp/rad/issues with:

  • The error message
  • Your script (or a minimal version that reproduces it)
  • Your Rad version (rad --version)
  • Your operating system

RAD20013: File Walk Error

An error occurred while traversing a directory with find_paths().

Common Causes

  • Permission denied - can't access a subdirectory
  • Symlink loops - circular symbolic links
  • Directory removed - directory deleted during traversal

How to Fix

dir = "."
files = find_paths(dir) catch:
    print_err("Failed to scan directory: {files}")
    exit(1)

Check that you have read access to all directories in the tree.

RAD20014: Mutually Exclusive Arguments

Two command-line arguments that can't be used together were both specified.

Example

args:
    verbose bool
    quiet bool

    quiet excludes verbose

Running with --verbose --quiet triggers this error.

How to Fix

Use only one of the conflicting options. Run --help to see which arguments are mutually exclusive.

RAD20015: Zip Strict Length Mismatch

zip() was called with strict=true on lists of different lengths.

Example

a = [1, 2, 3]
b = ["a", "b"]

pairs = zip(a, b, strict=true)  // Error: lengths 3 vs 2

How to Fix

Either make the lists the same length, or use non-strict mode (which truncates to the shortest):

a = [1, 2, 3]
b = ["a", "b"]
pairs = zip(a, b)  // Stops at shortest list: [[1, "a"], [2, "b"]]

RAD20016: Cast Failed

A value couldn't be converted to the requested type - typically a non-numeric string passed to parse_int / parse_float, or a value of the wrong shape passed to int(...) / float(...).

Example

x = parse_int("hello")  // "hello" isn't a number

parse_int and parse_float return an error value on failure, so the script aborts at the use site unless the error is handled.

How to Fix

Handle the error with ?? (default) or catch: (block):

x = parse_int("hello") ?? 0
y = parse_float("3.14") ?? 0.0

Or branch on whether the input looks parseable before attempting:

raw = "hello"
n = parse_int(raw) catch:
    print_err("not a number: {raw}")
    exit(1)

RAD20017: Number Out of Range

A numeric value is outside the valid range accepted by an operation.

Example

x = round(3.14159, -2)  // Error: precision must be non-negative

How to Fix

Check that values are within the bounds the operation accepts. For example, round requires a non-negative precision, to_json a non-negative indent, and range a non-zero step.

(An integer literal that doesn't fit in an int64 is a separate, compile-time error - see RAD40015.)

RAD20018: Empty List

An operation that requires at least one element was called on an empty list.

Example

items = []
first = items[0]       // No first element
max_val = max(items)   // Nothing to compare

How to Fix

Check the list length first:

items = []
if len(items) > 0:
    first = items[0]
else:
    first = "default"

Or use a ternary:

items = []
first = len(items) > 0 ? items[0] : "default"

RAD20019: Argument Constraint Violated

A command-line argument didn't meet its declared constraint.

Example

args:
    level str
    level enum ["low", "medium", "high"]

Running with --level ultra fails because "ultra" isn't in the allowed values.

Constraint Types

  • Enum - value must be one of the specified options
  • Regex - value must match a pattern
  • Range - numeric value must be within bounds

How to Fix

Run --help to see valid values. Constraints are case-sensitive.

RAD20020: FID Generator Error

Rad failed to generate a unique identifier (FID). This is a system-level issue, not a problem with your script.

How to Fix

Retry the operation. If the error persists, file a bug report at: https://github.com/amterp/rad/issues

RAD20021: Decode Error

The input couldn't be decoded because it's not valid for the specified encoding.

Example

// Invalid base64 - contains characters not in base64 alphabet
decode_base64("not valid base64!!!")

// Correct
decoded = decode_base64("SGVsbG8gV29ybGQ=")

How to Fix

Verify the data matches the encoding you're using. If the data comes from an external source, handle potential errors:

data = "SGVsbG8gV29ybGQ="
decoded = decode_base64(data) catch:
    print_err("Invalid base64 data")
    exit(1)

RAD20022: Stash ID Not Found

A stash function was called, but the script doesn't declare a stash ID.

Stashes scope persistent data per-script, so Rad needs to know which script owns the data. Set the stash ID with @stash_id in the file header.

Example

// Wrong: no @stash_id header, so any stash call fails at runtime.
result = load_stash_file("config.json", r'{"theme": "dark"}')

How to Fix

Add @stash_id to the file header:

---
@stash_id = my_script
---

// Now stash calls work. load_stash_file creates the file with the
// default content if it's not there yet. Pair with write_stash_file
// to update.
result = load_stash_file("config.json", r'{"theme": "dark"}')

To see existing stashes, run rad stash list.

See also: Stashes guide.

RAD20023: Invalid Sleep Duration

The sleep() function received a duration it doesn't understand.

Example

// Wrong
sleep("abc")      // not a valid duration
sleep("5x")       // unknown unit

// Correct
sleep(1.5)        // 1.5 seconds (numbers are seconds)
sleep("500ms")    // 500 milliseconds
sleep("2s")       // 2 seconds
sleep("1m")       // 1 minute
sleep("1h")       // 1 hour

Valid Units

Unit Meaning
ms milliseconds
s seconds
m minutes
h hours

Numbers without units are treated as seconds.

RAD20024: Invalid Regular Expression

The regex pattern has a syntax error.

Example

// Wrong: unclosed bracket
matches("hello", "[invalid")

// Correct
matches("hello", "[a-z]+")
matches("test123", "\\d+")

Common Mistakes

  • Unclosed [ or (
  • Quantifiers (*, +, ?) with nothing before them: "*abc" should be ".*abc"
  • Missing escape: use \\d for digits, \\. for literal dot

Rad uses Go's regex syntax. Test complex patterns with an online regex tester.

RAD20025: Colorize Value Not in Enum

A colorization function received a value that wasn't in its predefined set.

When you configure color-based formatting (such as in table columns), you define which values map to which colors. This error means the data contained a value not in that mapping.

How to Fix

Either add the missing value to your color mapping, or ensure your data only contains expected values before colorizing.

RAD20026: Stdin Read Error

Something went wrong while reading from standard input - the stream may have been closed or corrupted.

How to Fix

Check whether stdin is available before reading:

if has_stdin():
    data = read_stdin()
else:
    print_err("No input provided")
    exit(1)

RAD20027: Invalid Check Duration

An invalid duration was specified for an internal operation.

This error is used internally for testing. If you encounter it unexpectedly, please file an issue at https://github.com/amterp/rad/issues.

RAD20028: Undefined Identifier

You're referring to an identifier (a variable or function) that hasn't been defined yet.

Example

username = "alice"
print(usernme)  // Error: typo - 'usernme' doesn't exist

Common Causes

  • Typos - Check the spelling. Rad suggests similar names if it finds any.
  • Wrong order - The variable must be assigned before you use it.
  • Scope - Variables inside functions aren't visible outside:
fn setup():
    config = "value"  // only exists inside setup()

setup()
print(config)  // Error: config is not defined here

Note

Rad is case-sensitive: MyVar and myvar are different variables.

RAD20029: Index Out of Bounds

You tried to access an index that doesn't exist in the list or string.

Example

items = ["a", "b", "c"]  // valid indices: 0, 1, 2
print(items[3])  // Error: index 3 out of bounds

Lists are 0-indexed, so a list with 3 elements has indices 0, 1, and 2.

How to Fix

Check the length before accessing:

items = ["a", "b", "c"]
index = 3
if len(items) > index:
    print(items[index])

Negative indices work too: -1 is the last element, -2 is second-to-last. But going past the start (e.g., items[-10] on a 3-element list) still causes this error.

RAD20030: Break Outside Loop

break can only be used inside a loop. It exits the loop early.

Example

x = 10

// Wrong
if x > 5:
    break  // not inside a loop

// Correct
for i in range(10):
    if i > 5:
        break
    print(i)

Alternatives

  • To exit a function: use return
  • To exit the script: use exit()

See also: RAD20031 (Continue Outside Loop)

RAD20031: Continue Outside Loop

continue can only be used inside a loop. It skips to the next iteration.

Example

x = 10

// Wrong
if x < 20:
    continue  // not inside a loop

// Correct
for i in range(10):
    if i % 2 == 0:
        continue  // skip even numbers
    print(i)

See also: RAD20030 (Break Outside Loop)

RAD20032: Not Iterable

You tried to use for on something that can't be iterated.

Example

// Wrong: can't iterate over an int
for x in 42:
    print(x)

// Correct: iterate over a list, string, map, or range
for x in [1, 2, 3]:
    print(x)

Iterable Types

Type Iterates over
List elements
String characters
Map keys
Range numbers

Integers, floats, and booleans are not iterable.

RAD20033: Unpack Mismatch

The number of variables doesn't match the number of values being unpacked.

Example

// Wrong: 3 values, 2 variables
a, b = [1, 2, 3]

// Correct
a, b, c = [1, 2, 3]

How to Fix

Either adjust the variable count, or use indexing if you only need some values:

items = [1, 2, 3]
first = items[0]
last = items[-1]

RAD20034: Switch No Match

The switch value didn't match any case, and there's no default.

Example

status = "pending"
switch status:
    case "active":
        print("Active")
    case "inactive":
        print("Inactive")
// Error if status is something else like "pending"

How to Fix

Add a default case:

status = "pending"
switch status:
    case "active":
        print("Active")
    case "inactive":
        print("Inactive")
    default:
        print("Unknown status: {status}")

RAD20035: Switch Multiple Match

The value matched more than one case, and Rad refuses to silently prefer one over the other.

Example

name = "alice"
switch name:
    case "alice" -> print("ALICE")
    case "bob" -> print("BOB")
    case "charlie", name -> print("CHARLIE")
// Error: "alice" matches both the first and third cases

The third case binds name as one of its match keys, which makes any name value match - so an input of "alice" overlaps with the first case as well.

How to Fix

Make the cases mutually exclusive, or restructure as an if/else if chain if you need guard-style logic:

name = "alice"
if name == "alice":
    print("ALICE")
else if name == "bob":
    print("BOB")
else:
    print("CHARLIE")

When the cases are constant literals, the static checker catches the overlap as RAD40012 (Unreachable Case) before runtime. This runtime error remains the safety net for overlaps that only show up once the discriminant value is known.

RAD20036: Division by Zero

You can't divide or modulo by zero.

Example

x = 10 / 0   // Error
y = 10 % 0   // Error

How to Fix

Check the divisor before using it:

value = 10
divisor = 0
if divisor != 0:
    result = value / divisor
else:
    result = 0  // or handle appropriately

RAD20037: Negative Index

A negative index was used where it's not allowed.

Rad generally supports negative indexing (-1 for the last element), so this error is rare. If you encounter it unexpectedly, please report it at https://github.com/amterp/rad/issues.

RAD20038: Void Value

You tried to use the result of a function that doesn't return anything.

Example

fn greet():
    print("Hello!")
    // no return statement

result = greet()  // Error: greet() returns void

How to Fix

Either add a return value to the function:

fn greet():
    print("Hello!")
    return true

Or don't try to capture the result:

fn greet():
    print("Hello!")

greet()  // just call it

RAD20039: Unsupported Operation

The operation you tried isn't supported for this type.

This is a general error - the message will tell you which operation failed and on what type. Check that you're using the right types and operators for what you're trying to do.

RAD20040: Retired

This error code is no longer in use. The assert() built-in it was designed to flag was never implemented; this doc previously described a planned feature that didn't land.

The number stays reserved per the tombstone rule - we never reuse retired codes, so old logs that mention RAD20040 remain greppable.

For runtime invariant checks today, use an explicit if + exit():

items = [1, 2, 3]
if len(items) == 0:
    print_err("items must not be empty")
    exit(1)

RAD20041: Key Not Found

You tried to access a map key that doesn't exist.

Example

data = {"name": "Alice", "age": 30}
print(data["email"])  // Error: 'email' not found

How to Fix

Check first, or use a default:

data = {"name": "Alice", "age": 30}

// Check first
if "email" in data:
    print(data["email"])

// Or use the ?? fallback operator
email = data["email"] ?? "unknown"

RAD20042: Internal Bug

This is a bug in Rad, not your script. Something went wrong inside the interpreter.

What to Do

Please report this at https://github.com/amterp/rad/issues with: - The error message and stack trace - Your script (or a minimal version that reproduces it) - Your Rad version (rad --version) - Your OS

As a workaround, try restructuring the problematic code - sometimes a different approach avoids the bug.

RAD20043: Failed to Parse Duration

A string passed to parse_duration could not be interpreted as a valid duration.

Example

parse_duration("invalid!")  // Error: failed to parse
parse_duration("5m23s")     // OK: 5 minutes 23 seconds
parse_duration("1d12h")     // OK: 1 day 12 hours

Valid Format

Duration strings support these suffixes: w, d, h, m, s, ms, us/µs, ns. Combine them like "1w2d" or "5m30s". Spaces are allowed ("5m 30s"). A leading - negates the entire duration.

How to Fix

  • Ensure the string contains valid duration suffixes
  • Check for typos in the unit suffixes
  • Use catch to handle the error if the input is dynamic

RAD20044: Failed to Parse Date

A string passed to parse_date could not be interpreted as a valid date.

Example

parse_date("not-a-date")                          // Error: unrecognized format
parse_date("2026-03-22")                           // OK: ISO date
parse_date("2026-03-22T14:30:00Z")                 // OK: ISO datetime with timezone
parse_date("22/03/2026", format="DD/MM/YYYY")      // OK: custom format

Auto-Detected Formats

When no format is specified, parse_date tries these formats:

  • YYYY-MM-DDTHH:mm:ssZ or ...+HH:MM (RFC 3339, with optional fractional seconds)
  • YYYY-MM-DD HH:mm:ss+HH:MM (space-separated with timezone offset, with optional fractional seconds)
  • YYYY-MM-DDTHH:mm:ss (ISO datetime, with optional fractional seconds)
  • YYYY-MM-DD HH:mm:ss (space-separated, with optional fractional seconds)
  • YYYY-MM-DD (date only)

Format Tokens

When using the format parameter, these tokens are available:

Token Meaning Example
YYYY 4-digit year 2026
MM 2-digit month 03
DD 2-digit day 22
HH 2-digit hour (24h) 14
mm 2-digit minute 30
ss 2-digit second 00

Note: MM (uppercase) is month, mm (lowercase) is minute. Mixing these up will produce wrong results or parse errors.

Format tokens are replaced wherever they appear in the format string, including inside other text. Use format strings that contain only tokens and separator characters (e.g. DD/MM/YYYY, YYYY-MM-DD HH:mm:ss).

How to Fix

  • Check that your date string matches one of the auto-detected formats, or provide an explicit format
  • Ensure the format tokens match the structure of your date string
  • Use catch to handle the error if the input is dynamic

Type Errors (RAD3xxxx)

RAD30001: Type Mismatch

A value of one type was used where a different type was expected.

Example

fn double(n: int) -> int:
    return n * 2

x = "42"
double(x)  // Error: expected int, got string

How to Fix

Use the right type, or fix the function/variable annotation:

fn double(n: int) -> int:
    return n * 2

x = 42        // use int directly
double(x)

If you must convert from another type, note that the conversion functions (int, float, etc.) can fail and return T|error, so you need to handle the error case before using the result:

x = "42"
n = int(x) catch:
    print_err("Not a number: {x}")
    exit(1)
print(n)

Type Conversion Functions

Function Description
int(value) Convert to integer (or error)
float(value) Convert to float (or error)
str(value) Convert to string
bool(value) Convert to boolean

RAD30002: Invalid Type for Operation

An operator was used with types that don't support that operation.

Example

// Wrong: can't add int to string
x = "count: " + 5

// Correct: use interpolation (preferred)
x = "count: {5}"

// Correct: explicit conversion
x = "count: " + str(5)

Supported Type Combinations

Operator Works with
+ int+int, float+float, str+str, str+error, error+str, error+error, list+list
-, *, / int, float
% int
==, != any types
<, >, <=, >= int, float, str
and, or any (uses truthiness)

How to Fix

Use interpolation to mix types in strings - it handles any type automatically: "Value: {x}". Alternatively, convert types explicitly with int(), float(), or str().

v0.9 Migration Note: The + operator no longer coerces types. If you're seeing this error after upgrading, see the v0.9 migration guide.

RAD30003: Cannot Format

A value couldn't be formatted with the given format specifier.

Example

x = "hello"
print("{x:.2}")  // Error: precision (.2) needs a number, got string

// Fix: use a compatible specifier or convert first
print("{x}")               // Default works for any type
print("{parse_int('42'):,}")  // Convert before formatting

Format Specifiers

Specifier Requires Example
{x} any type Default formatting
{x:10} any type Right-align, width 10
{x:<10} any type Left-align, width 10
{x:*>10} any type Fill with *, right-align
{x:.<10} any type Fill with ., left-align
{x:05} any type Zero-pad shorthand, width 5
{x:<05} any type Zero-pad, left-align
{x:0>5} any type Explicit zero fill (any type)
{x:.2} number Precision (2 decimals)
{x:,} number Thousands separator
{x:010,} number Zero-pad + thousands

The zero-pad shorthand ({x:05}) works on any type. For numbers, it is sign-aware (negative signs placed before zeros). For other types, it simply prepends zeros (same as {x:0>5}).

RAD30004: Cannot Index

Indexing was attempted on a type that doesn't support it.

Example

// Wrong: can't index an integer
x = 42
print(x[0])

// Correct: index lists, strings, or maps
items = [1, 2, 3]
print(items[0])      // 1

text = "hello"
print(text[0])       // "h"

data = {"a": 1}
print(data["a"])     // 1

Indexable Types

Type Syntax
Lists items[0], items[-1], items[1:3]
Strings text[0], text[-1], text[1:3]
Maps data["key"]

To index a number's digits, convert it first: str(12345)[0] gives "1".

RAD30005: Cannot Assign

Assignment was attempted to something that can't be assigned to.

Example

items = [1, 2, 3]
data = {"k": "value"}

// Wrong
5 = x
get_value() = 5

// Right: assign to variables or index expressions
x = 5
items[0] = 10
data["key"] = "value"
a, b = [1, 2]

The left side of = must be a variable, index expression, or unpacking pattern.

RAD30006: Invalid Argument Type

A function received an argument of the wrong type.

Example

// Wrong: len() expects a collection
len(42)

// Correct
len([1, 2, 3])  // 3
len("hello")    // 5

The error message tells you what type was expected. Convert with int(), str(), list(), etc. if needed.

RAD30007: Wrong Argument Count

A function was called with too few or too many arguments.

Example

fn add(a, b):
    return a + b

add(5)        // Error: missing argument 'b'
add(1, 2, 3)  // Error: too many arguments

How to Fix

Check the function signature. Use rad docs reference/functions to see the expected arguments for built-in functions.

fn add(a, b):
    return a + b

add(5, 10)  // Correct: two arguments

Named arguments can help clarify which is which:

fn process(input, output):
    print("Processing {input} -> {output}")

value = "in.txt"
dest = "out.txt"
process(input=value, output=dest)

RAD30008: Cannot Compare

Two values cannot be compared with the given comparison operator.

This error code is reserved and may not currently appear in practice. Most types can be compared for equality, and ordering comparisons work on compatible types.

If you encounter this error, please report it: https://github.com/amterp/rad/issues

RAD30009: Cannot Convert

A type conversion is not possible between the given types.

This error code is reserved and may not currently appear in practice. Most conversions use dedicated functions like int(), str(), etc. which have their own error handling.

If you encounter this error, please report it: https://github.com/amterp/rad/issues

RAD30010: Collection Element Mismatch

A list element or map value was assigned a value that doesn't match the collection's declared element/value type. Indexed assignment (xs[i] = v or m[k] = v) is the only static mutation surface on typed collections today, so this is where the gap shows up.

Example

xs: int[] = [1, 2, 3]
xs[0] = "wrong"        // Error: 'str' not assignable to element 'int'

m: { str: int } = { "a": 1 }
m["b"] = "nope"        // Error: 'str' not assignable to value 'int'

How to Fix

Use a value of the declared element type:

xs: int[] = [1, 2, 3]
m: { str: int } = { "a": 1 }

xs[0] = 99             // int into int[]
m["b"] = 2             // int into { str: int }

Or, if the collection genuinely holds mixed values, widen the declared type so the assignment lands in scope:

xs: (int|str)[] = [1, "two", 3]
xs[0] = "wrong"        // ok: 'str' is in the (int|str) element type

When the Check Skips

The check is opt-in via annotation. Untyped locals (xs = [1, 2]) infer a non-narrow element type and don't trigger this diagnostic. Containers typed as the open list or map (no element type) also skip - they're already "any element goes."

RAD30011: Unhandled Fallible Call

Calling a function that can fail (returns ... | error) without handling the error case.

port_str = "8080"
port = parse_int(port_str)   // RAD30011: this call can fail; the error isn't handled
print(port + 1)

Why this happens

Functions like parse_int, parse_float, and parse_json return a union of a success type and error (e.g. int | error). At runtime, if the call fails and nothing handles the error, the script halts. The checker points this out so the failure path is a deliberate choice rather than an accident.

Note this is a hint, not an error: the script still runs, and succeeds whenever the call succeeds. The success type flows on (port above is int), so later uses type-check normally.

How to fix it

Handle the error with catch to supply a fallback value:

port_str = "8080"
port = parse_int(port_str) catch 8080
print(port + 1)

Or use ?? for the same effect more tersely:

port_str = "8080"
port = parse_int(port_str) ?? 8080
print(port + 1)

Use a catch block when the recovery needs more than a single fallback value:

port_str = "8080"
port = parse_int(port_str) catch:
    print("invalid port, using default")
    yield 8080
print(port + 1)

Validation & Lint Errors (RAD4xxxx)

RAD40001: Scientific Notation Not a Whole Number

Scientific notation was used where an integer is required, but the result has a fractional part.

Example

args:
    count int = 1.5e1  # Error: 15.0 is not a whole number

These produce whole numbers and are fine:

args:
    count int = 1e3    # 1000
    batch int = 2e2    # 200

What This Means

Scientific notation like 1e3 means 1 x 10^3 = 1000. When used where an integer is expected, the result must be a whole number. 1.5e1 produces 15.0, which has an implicit decimal part.

Use plain integers (1000) or ensure the notation produces an exact integer.

RAD40002: Function Shadows Argument

A function has the same name as a command-line argument.

Example

args:
    count int

fn count():  // Error: shadows the 'count' argument
    return 5

Rad hoists function definitions to the top of the scope, so this would hide the argument value and make it inaccessible.

How to Fix

Rename either the function or the argument:

args:
    count int

fn get_count():  // Renamed function
    return count

RAD40003: Unknown Function

A function was called that hasn't been defined.

Example

greet("Alice")  // Error: 'greet' is not defined

// Fix: define the function first
fn greet(name):
    print("Hello, {name}!")

greet("Alice")

How to Fix

  1. Check spelling - Rad suggests similar names if any exist
  2. Define the function - Add the function definition before the call
  3. Check scope - Ensure the function is visible where you're calling it

Built-in functions like print, len, and int are always available.

RAD40004: Return Outside Function

A return statement appeared outside of a function body.

Example

// Wrong
x = 10
return x  // Error: not inside a function

// Correct
fn get_value():
    return 10

result = get_value()

How to Fix

  • To end the script early, use exit(): exit(1) for failure, exit(0) for success
  • Check indentation - The return might be accidentally de-indented outside the function body
  • Wrap in a function if you need return semantics at top level

RAD40005: Yield Outside Switch Case

A yield statement appeared outside of a switch-case block. yield is how a multi-statement switch arm returns its result to the surrounding switch expression.

Example

// Wrong - yield at top level
yield 5  // Error: not inside a switch case

// Correct - yield inside a switch-case block
value = "a"
result = switch value:
    case "a":
        x = 10
        yield x * 2
    case "b" -> 20
    default -> 0

In single-expression switch arms you use -> instead; yield is only needed when an arm has multiple statements and wants to return a value.

RAD40006: Invalid Assignment Target

The left side of an assignment isn't something that can be assigned to.

Example

items = [1, 2, 3]
data = {"k": "v"}

// Wrong
5 = x
a + b = 10

// Valid assignment targets
x = 5              // Variable
items[0] = 10      // List element
data["key"] = "v"  // Map entry
a, b = [1, 2]      // Unpacking
x += 1             // Compound

The left side of = must be a variable, index expression, or unpacking pattern.

RAD40007: Rad Option Has No Effect

A rad block option was used in a context where it has no effect.

Example

// 'insecure' and 'quiet' only apply to URL sources, so using them
// on a sourceless rad block is pointless.
rad:
    insecure    // Warning: no URL to apply this to
    quiet       // Warning: no URL to apply this to
    fields Name

// 'noprint' has no effect without a source because sourceless rad
// blocks use a save/restore pattern -- mutations aren't preserved.
rad:
    noprint     // Warning: mutations aren't preserved anyway
    fields Name

Note: when a source expression is provided (e.g. rad data:), the source could resolve to either a URL or a list/map at runtime, so no static warning is emitted -- both code paths are legitimate.

How to Fix

  1. Remove the option if it's not needed
  2. Use a URL source if you need insecure or quiet
  3. Add a source if you need noprint to suppress printing

RAD40008: Deprecated Block Keyword

The request and display block keywords have been removed in v0.9. All rad block variants now use the unified rad keyword.

Migration

Replace request and display with rad:

data = [{"name": "Alice", "age": 30}]
Name = json[].name
Age = json[].age

// Before (no longer works)
request "https://api.example.com/data":
    fields Name, Age

display data:
    fields Name, Age

// After
rad "https://api.example.com/data":
    noprint
    fields Name, Age

rad data:
    fields Name, Age

Important: request blocks never printed a table - they only fetched data and populated fields. The unified rad block prints by default, so add noprint when migrating from request if you don't want table output.

The rad keyword now handles all source types:

  • URL source: rad "https://...": (replaces request - add noprint to keep old behavior)
  • List/map source: rad myData: (replaces display)
  • No source: rad: (replaces display with no argument)

See the migration guide for full details.

RAD40009: Duplicate Parameter

A function or lambda has two parameters with the same name. Parameter names must be unique within a single parameter list so the body can refer to each one unambiguously.

Example

// This is an error: 'x' is declared twice.
fn add(x, x):
    return x + x

Fix

Rename one of the parameters so each binding has a distinct name:

fn add(a, b):
    return a + b

Shadowing an outer-scope name with a parameter is fine - that's a common pattern. This error only fires when two parameters of the same function or lambda collide with each other.

RAD40010: Non-Exhaustive Switch

A switch over a closed type (today: string-enum) doesn't cover every value in the type, and there's no default arm to catch the rest. At runtime the unmatched values would fail with ErrSwitchNoMatch (RAD20034); the static check surfaces the gap before the script runs.

Example

// 'c' is not covered.
fn render(name: ["a", "b", "c"]):
    switch name:
        case "a":
            print("AAA")
        case "b":
            print("BBB")

Fix

Either add a case for the missing value:

fn render(name: ["a", "b", "c"]):
    switch name:
        case "a":
            print("AAA")
        case "b":
            print("BBB")
        case "c":
            print("CCC")

...or add a default arm to catch the rest:

fn render(name: ["a", "b", "c"]):
    switch name:
        case "a":
            print("AAA")
        default:
            print("other")

Open-typed discriminants (plain str, int, etc.) never trigger this warning. Exhaustiveness only applies when the static type is a finite set the checker can enumerate.

RAD40011: Duplicate Typed Declaration

A name was declared with a type annotation (x: T = ...) when x was already declared in the same scope. The declared type is part of the binding's contract: re-declaring would either silently change the type out from under earlier assignments or duplicate information the reader has to reconcile. Either way it's a refactor hazard, so the static checker rejects it.

Example

// This is an error: 'bb' was already declared on line 1.
bb: int = 5
bb: str = "hi"

Same-type re-declarations are flagged for the same reason - they're almost always leftover scaffolding from an edit:

// Also an error.
bb: int = 5
bb: int = 6

Fix

If you want to reassign, drop the annotation. The original declared type still applies, and the checker enforces it:

bb: int = 5
bb = 6

If you really meant to introduce a new variable, give it a fresh name. Rad doesn't currently support intentional re-declaration in the same scope.

RAD40012: Unreachable Case

A switch case key matches a value that was already matched by an earlier arm. The later arm can never be reached at runtime, so it's almost always dead code - either a copy-paste mistake or leftover scaffolding from an edit.

Example

name = "a"

// This is an error: 'a' is matched by the first case.
switch name:
    case "a":
        print("first")
    case "a":
        print("second")

Multi-key cases get the same treatment - any single key that collides with an earlier arm is flagged:

name = "b"

// 'b' on line 4 is unreachable.
switch name:
    case "a", "b":
        print("first")
    case "b", "c":
        print("second")

Fix

Remove the redundant key. If you wanted both branches to execute the same body, merge them:

name = "a"
switch name:
    case "a", "b":
        print("matched")

Or, if the second arm was supposed to match a different value, correct the key:

name = "a"
switch name:
    case "a":
        print("first")
    case "b":
        print("second")

Only static literals are checked (strings, ints, floats, bools). Cases keyed by variables, function calls, or other expressions are left alone - the analyzer can't prove they collide.

RAD40013: Case Key Not In Discriminant Type

A switch case key has a value that the discriminant can never hold. The arm is unreachable not because of an earlier case (that's RAD40012), but because the discriminant's type statically forbids the value.

This typically surfaces after a refactor: an enum variant gets renamed and the old spelling lingers in a switch.

Example

fn classify(name: ["a", "b", "c"]):
    // 'wat' is not in the closed type ["a", "b", "c"];
    // `name` can never equal it.
    switch name:
        case "a":
            print("first")
        case "b":
            print("second")
        case "c":
            print("third")
        case "wat":           // RAD40013
            print("unreachable")

Fix

Drop the stale arm:

fn classify(name: ["a", "b", "c"]):
    switch name:
        case "a":
            print("first")
        case "b":
            print("second")
        case "c":
            print("third")

Or correct the key to one of the type's allowed values:

fn classify(name: ["a", "b", "c"]):
    switch name:
        case "a":
            print("first")
        case "b":
            print("second")
        case "c":
            print("third")

When the check fires

The check only fires when the discriminant has a closed value set the analyzer can enumerate. Today that means string-enum types (e.g. ["a", "b", "c"]). Open str / int discriminants have no enumerable domain, so the check stays silent.

Only static literal keys are checked. Cases keyed by identifiers, calls, or other expressions are left alone - the analyzer can't prove they fall outside the domain.

RAD40014: Unknown Map Key

A map access reads a key that the map's type doesn't declare. The access is guaranteed to fail at runtime (RAD20041, key not found), so the analyzer flags it statically.

This fires only when the receiver has a closed map shape the analyzer can enumerate - typically a builtin's typed-map return value (e.g. get_path) or an explicitly annotated struct-typed map. Untyped map values have no declared keys, so the check stays silent for them.

Example

gp = get_path("README.md")
print(gp.full_path)   // ok: 'full_path' is a declared key
print(gp.fjull_path)  // RAD40014: typo - no such key

Fix

Use a declared key:

gp = get_path("README.md")
print(gp.full_path)

If the key you want is optional (marked ? in the type, e.g. get_path's size_bytes?), it's a valid key and won't fire this - but it may be absent at runtime. Guard the access so a missing key doesn't error (RAD20041).

When the check fires

Only reads are checked. Writing a key that isn't declared (gp.extra = 1) is allowed - it adds the key at runtime - so it does not fire.

Both dot access (gp.full_path) and string-literal index access (gp["full_path"]) are checked. Dynamic keys (gp[k]) and keys built from interpolation can't be resolved statically, so they're left alone.

Optional keys (declared with ?, e.g. get_path's size_bytes?) are considered valid keys: accessing one resolves its type and does not fire here, even though the key may be absent at runtime.

RAD40015: Integer Literal Out of Range

An integer literal was written whose value does not fit in a signed 64-bit integer. Rad integers are 64-bit, so a literal must be in the range -9223372036854775808 to 9223372036854775807.

Example

x = 9223372036854775808   // Error: one past the maximum int64

These are fine:

biggest = 9223372036854775807   // the maximum int64
million = 1_000_000             // underscores are allowed as digit separators
print(biggest, million)

What This Means

The literal's magnitude exceeds what an int can hold. Note that the most negative value cannot be written directly as -9223372036854775808, because the literal 9223372036854775808 is parsed (and range-checked) on its own before the unary minus applies.

Write the minimum value as -9223372036854775807 - 1, or use a float if you need to represent larger magnitudes (with reduced precision).