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.

RAD10023: '#' Comment

A # was used to start a comment, but Rad comments use //.

Example

// Wrong
# compute the total

// Correct
// compute the total

The # habit usually comes from Python or shell. In Rad, # starts a comment in exactly two places: the shebang line (#!/usr/bin/env rad) and inside args blocks, where # ... after an argument declaration documents that argument. Everywhere else, use //.

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 rad docs migrations/v0.9.

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

RAD20045: Invalid Shell Command Value

A value used as a shell command, or interpolated into one, has no form Rad can safely pass to a program.

Inside a shell command, the text you write is shell and the interpolations are data. Rad quotes each interpolated value so it arrives as exactly one argument. That only works for values with an obvious one-argument spelling: strings, numbers and booleans. A map has none, and a null has one that is silently wrong - it would become the four-character word null.

Examples

tag = null
$`git tag {tag}`        // Error: nothing sensible to pass

config = { "depth": 2 }
$`git clone {config}`   // Error: a map is not an argument

A list is allowed - it expands to one argument per element - but it has to stand alone as its own argument, because there's no single obvious meaning for gluing several arguments onto a prefix:

files = ["a.txt", "b.txt"]

$`rm {files}`           // Fine: rm a.txt b.txt
$`rm --file={files}`    // Error: which element gets the prefix?

The same rules apply when the whole command is a list:

$["git", "commit", "-m", null]   // Error
$[]                              // Error: no program to run

How to Fix

Give a null an explicit fallback, or leave the argument out entirely:

tag = null

$`git tag {tag ?? "untagged"}`

Build up the arguments as a list when some are conditional. Each element is one argument, so nothing needs quoting:

message = "fix the parser"

cmd = ["git", "commit"]
if message:
    cmd += ["-m", message]
$cmd

Join a list yourself when you really do want one argument:

files = ["a.txt", "b.txt"]

$`tar -czf out.tgz {files}`             // three arguments
$`echo --files={files.join(",")}`       // one argument

Why Rad Works This Way

Bash's defining hazard is that a value carrying a space, a quote or a $ stops being a value and becomes syntax. Rad removes that by quoting interpolations for you - but quoting only has an answer for things that are one argument. Rather than invent a rendering for the rest and let it fail silently at runtime, Rad asks you to say what you meant.

See Also

  • rad docs guide/shell-commands - the three command forms and when to use each

RAD20046: Interactive Input Required

The script asks the user something - via input(), confirm(), pick(), multipick(), or a confirm-gated shell command - but there is no terminal to ask at. Rad stops before running any of the script and tells you what it would have asked.

Rad looks for a terminal in two places: standard input, and /dev/tty (the terminal attached to your session, which works even when stdin is a pipe or a here-string). Both being unavailable means nothing interactive can happen: a CI job, a cron run, or an AI agent's tool call.

Nothing in the script has executed at this point. That is deliberate - discovering a prompt halfway through would leave you reasoning about a half-finished run before you could retry.

Rad exits 7 here, so a wrapper can tell "this run needs input" apart from "this run failed" without reading the message.

How to Fix

Answer each prompt up front with --reply, keyed by the line it sits on. Rad prints your own command back with a --reply per prompt: keys in place, answers left blank.

rad deploy.rad prod --reply '5:<yes|no>' --reply '6:<value>' --reply '7:<option>'

Replace the blanks and run it. Rad never fills them in - whether you mean yes, or which option you want, isn't in the script for it to read:

rad deploy.rad prod --reply 5:yes --reply 6:'shipped it' --reply 7:web-1

Answers are matched by kind, which is what the blank names. Nothing needs escaping except a multipick:

Prompt Blank Answer
confirm, confirm $\...`||yes/no(alsoy,n,true,false`)
input <value> the rest of the value, taken verbatim
pick, pick_kv, pick_from_resource <option> one option, matched exactly
multipick <option,...> comma-separated; \, for a literal comma, \\ for a backslash

Only the first colon separates the key from the value, so --reply 6:https://example.com needs no quoting. A blank left unreplaced is taken literally: confirm, pick, and multipick reject it, and input answers with the text <value>.

A multipick answer must name each option at most once and satisfy the prompt's min and max. Give an empty value (--reply 3:) to select nothing.

Repeating a Prompt

A prompt inside a loop runs more than once. Repeat the key to answer each pass in order:

rad cleanup.rad --reply 12:yes --reply 12:no

Answers are queued per line rather than globally, so adding a prompt earlier in a script never silently re-targets a later answer. Running out mid-loop stops the script rather than reusing the last answer.

A prompt in a function called from several places repeats too, and then one key stands for several different questions:

fn ask(label):
    return input("Enter {label}: ")

host = ask("hostname")
user = ask("username")
print("{user}@{host}")

Answers bind in the order the calls run. Where rad can account for every execution it lists them, so that order is readable without opening the file:

deploy.rad:2  input may run more than once - repeat --reply per run
    reached from:
      4  host = ask("hostname")
      5  user = ask("username")

It says nothing where it cannot account for them all - a call inside a loop, a function passed around as a value, a caller that itself runs more than once, or a recursive function. A list missing one call would read exactly like a complete one.

Rad cannot count the passes for you either way; that depends on the script's own data. Where stopping partway would cost you something, read the script and count before you answer.

Prompts You Don't Expect To Reach

If a prompt sits on a branch this run won't take, say so instead of inventing a value:

rad deploy.rad --dry-run --reply-na 20

If the script reaches it anyway, rad fails cleanly (see RAD20047) rather than acting on a guess. Use this for a pick given a filter too, since a filtered pick often narrows to a single option and never asks. Where the options and the filter are both literal, rad works out the survivor itself and writes the --reply-na for you; where either is computed while the script runs, that call is yours to make.

Secret Inputs

A secret input cannot be answered this way at all. Command-line arguments are visible to other processes on the machine, so a password there would leak. Run the script where a terminal is available, or use --reply-na if this run won't reach it. Rad refuses the answer whether secret is a literal or computed while the script runs, though only a literal can be reported here rather than at the prompt.

Interactive Functions Used As Values

One shape takes no --reply: an interactive function passed around as a value rather than called.

ask = pick
answer = ask(["dev", "prod"])

Answers are keyed by the position of the call, and rad can't see where such a value ends up or how often it runs, so there is nothing for a value to attach to. Use --reply-na on its line to assert this run never invokes it - which is the usual answer for a handler map whose interactive entry isn't the one selected. If that assertion is wrong, the script stops at the prompt (see RAD20047) rather than guessing.

Scripts That Disable Global Options

A script setting @enable_global_options = 0 removes --reply along with every other global flag, so its prompts can't be answered from the command line. Rad says so rather than suggesting a command the script would reject. Run it where a terminal is available, or re-enable global options in the script.

  • RAD20047 - a prompt was reached that couldn't be answered.
  • RAD20010 - an interactive prompt failed or was canceled.

RAD20047: Prompt Reached Without A Usable Answer

The script reached a prompt that --reply couldn't answer. Unlike RAD20046, this one fires mid-run, because it depends on something rad could not know before starting.

Whichever way you got here, the script ran up to this point. Check what it already did before you re-run it: a retry starts from the top.

The Prompt Was Marked Unreachable

You passed --reply-na for this line, asserting the run wouldn't reach it, and it did:

rad deploy.rad --reply-na 20

That assertion is a promise, not a fallback, so rad stops rather than guessing an answer. Either the branch you expected to skip was taken, or the assertion was aimed at the wrong line. Replace it with a real --reply, or work out why the branch ran.

The Answers Ran Out

A prompt inside a loop consumed every answer supplied for its line and was reached again:

rad cleanup.rad --reply 12:yes --reply 12:yes    # ran a third time

Rad does not reuse the last answer. One yes silently approving five hundred deletions is exactly the accident worth failing over. Supply as many answers as the loop has passes, or narrow what the loop iterates over.

A second shape lands here too, and rad cannot tell it from the first. A script that re-asks until it gets a value it accepts spends an answer on every attempt:

env = ""
while env not in ["prod", "staging"]:
    env = input("Environment? ")

--reply 3:dev is rejected, the loop comes back around, and there is nothing left to give it. Adding flags cannot fix this one - --reply 3:dev --reply 3:dev fails in exactly the same place. Answer with a value the script takes.

The message names both readings because only you can tell which applies. Count the passes if the prompt sits in a loop; check the value if the script validates it.

The Answer Matched No Option

A pick or multipick over runtime data - a fetched list, a resource file, a computed set - got an answer that isn't in it:

fn fetch_servers():
    return ["web-1", "web-2"]

server = pick(fetch_servers())

Rad can't check that up front, because the options don't exist until the script builds them. The error lists the real options, so the next run can name one exactly. Matching is exact by design: a near-miss fails rather than quietly acting on the wrong choice.

The Filter Already Chose

A pick given a filter that leaves one option takes it without asking. An answer naming anything else is a disagreement, and rad won't settle it by picking a side:

services = ["api", "worker", "scheduler"]
target = pick(services, "api", prefer_exact=true)

Here --reply 2:worker asks for one service and the script's own filter chose another. Usually the filter is the thing to change - it is normally built from an arg, so worker belongs there rather than in the answer.

An answer that names the option the filter settled on is consumed and the run carries on. Consuming it either way is what keeps a loop's answers in step with its passes, so a pick that settles on some passes and asks on others still lines up.

The Input Was Secret

input with secret set can't be answered from the command line at all - other processes on the machine can read your arguments. Where secret is written literally, RAD20046 says so before the script runs. Where it's computed, rad only finds out on reaching the prompt:

env = input("Environment")
needs_secret = env == "prod"
token = input("Token", secret=needs_secret)

Run it where a terminal is available, or use --reply-na if this run shouldn't reach it.

The Answer Broke A multipick's Rules

A multipick answer named an option twice, or gave more or fewer selections than the prompt allows. Where the bounds are literal, RAD20046 catches this up front; where they're computed, it surfaces here.

How to Fix

Rad prints the remaining prompts along with the failure, so a single re-run with the corrected --reply usually finishes the job.

  • RAD20046 - prompts need answers, raised before the script runs.
  • RAD20010 - an interactive prompt failed or was canceled.

RAD20048: Shell Command Exited Non-Zero

A shell command finished with a non-zero exit code and nothing handled it.

Shell commands are critical by default in Rad: a command that fails raises an error, and an error nobody handles ends the script. That's the opposite of bash, where a failing command is ignored unless you opt into set -e.

Examples

$`false`            // Error: Command exited with code 1
print("unreached")

The exit code carries through from whatever ran, so it tells you what happened:

$`grep nothing /etc/hosts`   // Error: Command exited with code 1 - grep found no match

How to Fix

Decide whether the failure is fatal. If it is, you may want a better message than the default:

$`make build` catch:
    print_err("Build failed - is the toolchain installed?")
    exit(1)

If it isn't, say so. catch: pass continues without comment:

$`rm -f /tmp/scratch` catch:
    pass

If the exit code is data rather than a failure - grep exiting 1 means "no match", not "something went wrong" - capture it. Capturing the code doesn't stop the error, so pair it with catch::

code, stdout = $`grep hello /etc/hosts` catch:
    stdout = ""

if code == 0:
    print("found: {stdout.trim()}")
else:
    print("no match")

Two Failures That Aren't The Command's

Two cases report through this error even though the command never produced the code itself:

  • You declined a confirm. Declining reports exit code 1, so a declined command behaves exactly like one that ran and failed. Capture targets are set to empty output rather than left undefined.
  • The command couldn't be started at all. A binary that isn't installed reports 127, and one that isn't executable reports 126 - the same numbers a POSIX shell uses. A line on stderr names what went wrong.
$["definitely_not_installed"] catch:
    print("not available, skipping")

See Also

  • rad docs guide/shell-commands - capture, modifiers, and the three command forms
  • rad docs guide/error-handling - ??, the catch operator, and catch: blocks

RAD20049: Construct Not Available In The REPL

You typed an args block, a command block, or a file header at the REPL. All three describe how a script is invoked from the command line, and a REPL session was never invoked from anywhere.

An args block declares the flags and positionals rad parses out of argv before the script runs. A command block declares a subcommand. A file header is the docstring rad prints in that script's usage. None of them has a meaning at a prompt, and the REPL says so rather than accepting them and doing nothing.

Examples

args:
    name str
    count int = 1
command greet:
    name str
    calls do_greet

fn do_greet():
    print("hello {name}")

How to Fix

Put the script in a file, then load it:

:load ./greet.rad

:load runs the file's statements against your session, so functions and variables it defines are yours to use afterwards. Its args block is skipped, because there are still no command-line arguments to parse.

To work on the values an args block would have produced, assign them:

name = "world"
count = 3

To try the real argument parsing, run the script:

rad greet.rad --name world

See Also

  • rad docs guide/repl - what a session can and cannot do
  • rad docs guide/args - declaring a script's command-line interface

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

count = 5

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

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

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

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 rad docs migrations/v0.9.

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 rad docs migrations/v0.9 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).

RAD40016: Regex Pattern Without regex=true

You passed something that reads like a regex to split() or replace(), but neither treats its pattern as a regex unless you ask.

line = "alice   30   admin"

// Warns: '\s+' is a regex, but split() is looking for that literal text
fields = split(line, "\s+")

// Correct
fields = split(line, "\s+", regex=true)

Why This Exists

Before v0.12, split() and replace() always treated their pattern as a regex. That default was backwards - most calls want literal text - and it failed silently in the dangerous direction: a . in a pattern quietly matched every character.

The default is now literal. The catch is that scripts written for the old behavior keep running. They just stop matching:

print(split("a b  c", "\s+"))   // -> ["a b  c"], not ["a", "b", "c"]

No error, no crash, only a wrong answer. This warning is the tap on the shoulder.

How to Fix It

If you meant a regex, add regex=true:

text = "order 66 shipped"
print(replace(text, "\d+", "N", regex=true))   // -> "order N shipped"

If the text really is literal, you can ignore the warning - the call already does what you want, and you no longer need to escape anything:

version = "1.2.3"
print(replace(version, ".", "-"))   // -> "1-2-3", no escaping needed

Ignore It When

The heuristic looks for constructs that rarely appear in literal text: \d, \s, a bracketed class, a group, a quantifier, alternation with branches on both sides, or a leading ^ / trailing $. Real text does sometimes contain those - "C++" and "a|b" are perfectly good literal separators. The warning is advice, not a verdict.

See rad docs migrations/v0.12 for the full picture, and rad check --from-logs all to sweep every script you've run recently.

RAD40017: Interpolating a Constant Literal

An interpolation contains a literal value, so it produces exactly the text you already wrote. "{4}" and "4" are the same string.

This is almost always a literal brace that got read as an interpolation.

Example

// Wrong: {4} interpolates the number 4, so this is the regex \d4
pattern = "\d{4}"

// Correct: escape the brace
pattern = "\d\{4}"

// Correct: a raw string, where nothing interpolates
pattern = r"\d{4}"

Regex quantifiers are where this bites. \d{4} looks like "four digits" and parses as "a digit, then the number 4". Wider quantifiers like \d{2,3} are a syntax error instead, so the single-number form is the only one that fails quietly.

How to Fix

  1. Escape the brace with \{ if you meant a literal {
  2. Use a raw string (r"...") if the text is brace-heavy - regexes and JSON templates usually are
  3. Write the value directly if you really did mean to interpolate a constant

Note that r"..." turns off all interpolation and escapes for that string, so it is the wrong tool if you also need {name} to expand.

Why This Is a Warning

The rule is syntactic: it fires only when the interpolated expression is itself a literal. Interpolating a variable that happens to hold a constant is fine and stays quiet.

digits = 4
name = "ana"
print("{digits}")      // no warning
print("{2 + 2}")       // no warning
print("{name:<12}|")   // no warning - 12 is a format width, not the value

RAD40018: Misleading Shell Capture Name

A shell capture target is named after one stream but receives another.

// Warns: 'output' isn't a reserved name, so the whole statement is
// positional - and position 0 is stdout, so 'code' gets the output.
code, output = $`git status --porcelain`

// Correct - all three names are reserved, so assignment goes by name
stdout, code = $`git status --porcelain`

The Two Rules

Shell commands produce three things: stdout, stderr, and an exit code. How your variables receive them depends on what you called them.

Named - when every target is spelled exactly code, stdout, or stderr, each one gets the stream it's named after and order doesn't matter:

stderr = $`cmd`               // just stderr
code, stdout = $`cmd`         // exit code and stdout
stderr, stdout, code = $`cmd` // all three, any order

Positional - the moment one target isn't a reserved name, the whole statement switches to filling (stdout, stderr, code) in order:

out = $`cmd`             // out = stdout
out, err = $`cmd`        // out = stdout, err = stderr
out, err, c = $`cmd`     // out = stdout, err = stderr, c = exit code

This warning fires where those two rules collide: you used a reserved name, but something else in the statement dropped you into positional mode, so the name means nothing and the slot decides.

How to Fix It

Either name every target so assignment goes by name:

// Before
code, output = $`cmd`
// After
code, stdout = $`cmd`

Or drop the reserved names and let position do the work:

// Before
code, output = $`cmd`
// After - now positional throughout, and reads as it behaves
output, _, code = $`cmd`

The first form is usually what you want. It's a rename, and it captures exactly the same streams.

Migrating From v0.11

Before v0.12, positional captures filled (code, stdout, stderr) - the exit code first. They now fill (stdout, stderr, code).

Scripts written against the old order still parse and still run. They just bind different variables:

// v0.11: version = stdout.   v0.12: version = stderr.
_, version = $`rad -v`

When the warning mentions v0.12, that's the case it found. The fix is usually to drop a slot, because what used to need two targets now needs one:

version = $`rad -v`

The exit code moved last because Rad already turns a non-zero exit into an error - you reach for catch: rather than for the code - and because a single capture binding the exit code was the least useful thing it could bind.

What This Warning Cannot Catch

It reads names, so a positional capture whose names give no signal is invisible to it:

a, b = $`cmd`   // no warning possible

Sweep every script you've run recently with rad check --from-logs all, and see rad docs migrations/v0.12 for the full picture.

RAD40019: Two Default Commands At One Level

Only one command per level runs when the user names none, so two default markers among siblings leave nothing to decide between them.

command add:
    tokens str
    default
    calls do_add

command edit:
    tokens str
    default          // Error: 'add' is already the default here
    calls do_edit

How to Fix

Keep the marker on whichever command should run when none is named, and drop the other:

command add:
    tokens str
    default
    calls do_add

command edit:
    tokens str
    calls do_edit

Nesting scopes this: each namespace picks its own default independently, so remote and image can each have one.

command remote:
    command add:
        name str
        default
        calls do_remote_add

command image:
    command build:
        tag str
        default
        calls do_image_build

See Also

  • rad docs guide/script-commands - commands, nesting and defaults

RAD40020: Command Does Nothing

A command either runs something or routes to sub-commands. This one does neither: it has no calls line and no nested command blocks, so invoking it could only ever do nothing.

command deploy:
    env str          // Error: nothing to run

How to Fix

Add a calls line naming the function to run:

command deploy:
    env str
    calls do_deploy

fn do_deploy():
    print("Deploying to {env}...")

For a short body, an inline lambda avoids naming a function you only use once:

command deploy:
    env str
    calls fn():
        print("Deploying to {env}...")

Or nest command blocks inside it, which makes it a namespace that routes to them instead of running itself:

command deploy:
    timeout int = 30     // shared with every sub-command

    command staging:
        calls do_staging

    command prod:
        calls do_prod

See Also

  • rad docs guide/script-commands - commands, nesting and defaults

RAD40021: Namespace Has A Callback

A command containing sub-commands is a namespace: it groups them and shares its args with them, and the sub-command the user names is what runs. So a calls line on it would never fire.

command remote:
    calls do_remote      // Error: 'remote' routes to its sub-commands

    command add:
        name str
        calls do_add

How to Fix

Drop the calls line. Args declared on the namespace still reach every descendant, which is usually what the callback was reaching for:

command remote:
    timeout int = 30     // inherited by add, and by anything nested deeper

    command add:
        name str
        calls do_add

If you wanted tool remote on its own to do something, mark one of its sub-commands default - that is the command it runs when none is named:

command remote:
    command list:
        default
        calls do_list

    command add:
        name str
        calls do_add

Why Rad Works This Way

Shape decides role, so there is no keyword to remember and no way for the declaration and the behavior to disagree. A block with calls runs; a block with commands inside routes.

See Also

  • rad docs guide/script-commands - commands, nesting and defaults

RAD40022: Default On A Namespace

default marks the command to run when the user names none. A namespace does not run - it routes - so marking one leaves the question unanswered.

command remote:
    default              // Error: 'remote' contains sub-commands

    command add:
        name str
        calls do_add

How to Fix

Mark the sub-command that should run:

command remote:
    command add:
        name str
        default
        calls do_add

    command remove:
        name str
        calls do_remove

tool remote origin now runs do_add, and so does tool remote add origin. The full form stays available, which is how you pass a value that happens to match a sibling's name - tool remote add remove adds a remote called "remove". tool remote -- remove does the same.

See Also

  • rad docs guide/script-commands - commands, nesting and defaults

RAD40023: Quoted Interpolation In A Shell Command

An interpolated value in a shell command is wrapped in quotes the script wrote itself. Rad already quotes interpolated values, so these quotes are no longer protecting anything - they end up inside the argument as literal characters.

message = "hello"

$`git commit -m "{message}"`   // Error: the quotes land in the message

How to Fix

Delete the quotes. Rad quotes the value for you, whatever it contains:

message = "hello"

$`git commit -m {message}`

When the argument is literal text plus a value, build the string first and interpolate that. The quotes were holding the two halves together as one argument; a single interpolation does that on its own:

version = "1.2.3"

// Wrong - the quotes become part of the commit message
$`git commit -m "Bump to {version}"`

// Correct
message = "Bump to {version}"
$`git commit -m {message}`

Why Rad Works This Way

Hand-written quotes could never be correct for every value. Double quotes still expand $HOME and $(whoami); single quotes cannot contain an apostrophe at all. So a command that worked for hello would break, or worse silently misbehave, on it's or 100% of $USERS.

Rad quotes each interpolated value at the point it knows what the value is, so the argument the program receives is exactly the value you had:

message = "it's fine"

$`git commit -m {message}`
// runs: git commit -m 'it'\''s fine'

The rule is that the text you write is shell - pipes, redirects, && and all - and interpolations are data.

See Also

  • rad docs guide/shell-commands - the three command forms
  • rad docs RAD20045 - values that have no single-argument form

RAD40024: Constraint Does Not Fit The Arg's Type

An arg carries a constraint its type cannot be checked against - an enum or regex on a number, or a range on a string.

args:
    scores int[]
    scores enum ["low", "high"]   // Error: scores holds ints

How to Fix

Use the constraint that matches the type:

Constraint Works on
enum str
regex str
range int, float
len any list or variadic

So the example above wants either a string arg:

args:
    levels str[]
    levels enum ["low", "high"]

or a numeric bound:

args:
    scores int[]
    scores range [0, 100]

A list or variadic takes the same constraints as the type it holds, because the constraint describes each element. scores int[] is checked with range, the same as a plain scores int - every value has to be in range, not the list as a whole. To bound how many values arrive instead, use len.

Why Rad Works This Way

Rad used to accept these declarations and ignore them. A script could say scores range [0, 100] on an int[], pass --scores 500, and get no complaint - the constraint reached the parser and was dropped there. That is worse than rejecting it: the script reads as though it validates its input, so whoever wrote it stops checking by hand, and the values flow on unexamined.

A constraint that cannot be applied is a mistake in the script rather than a preference, so Rad reports it instead of guessing at what was meant.

See Also

  • rad docs guide/args - declaring args and their constraints

RAD40025: Shell Command Used Without a Result Accessor

A shell command was used where a value is wanted, but nothing said which result. Add .stdout, .stderr, .code, or .ok.

if $`git status --porcelain`:      // Error: which result?
    print_err("Uncommitted changes!")

How to Fix

Pick the accessor that matches the question you're asking:

// "did it print anything?"
if $`git status --porcelain`.stdout.trim():
    print_err("Uncommitted changes!")

// "did it succeed?"
if not $`which docker`.ok:
    exit(1)

Which Accessor

Accessor Type Fails on non-zero exit
.stdout str yes
.stderr str yes
.code int no
.ok bool no

.stdout and .stderr fail because output a failed command never produced isn't an answer. .code and .ok never fail - asking about the outcome is handling it, which is what makes them the right choice for testing a command.

Capture follows the accessor, exactly as it follows target names in a named assignment: reading .stdout still lets stderr through to the terminal.

Why There's No Default

A bare command is ambiguous in a way that bites. In shell, if cmd tests the exit code, but if $(cmd) tests whether it printed anything - two different questions that look nearly identical. Requiring the accessor means the reader never has to guess which one you meant.

Running a Command, Not Reading One

If you only want to run the command, it's already a statement - no accessor needed:

$`make build`
stdout = $`make build`

See Also

  • rad docs guide/shell-commands - capture, modifiers, and the three command forms
  • rad docs RAD40026 - postfix that isn't an accessor

RAD40026: Postfix After a Shell Command Isn't a Result Accessor

$ takes only the command, so whatever follows reads the command's result. The only things that can follow are .stdout, .stderr, .code, and .ok.

This fires on three shapes, and the first two used to mean something else.

An Index or a Call on the Command

cmds = ["echo a", "echo b"]
$cmds[1]                  // Error

$ used to swallow the whole expression, so this indexed cmds first and ran whatever came back. Now $ takes cmds as the command, and [1] is postfix with nothing to index. Wrap the command in parentheses to get the old meaning:

$(cmds[1])

That form has always parsed and always run, so this is a mechanical fix. The same applies to any computed command:

$(parts.join(" "))

A Method on the Command

$`echo hi`.upper()        // Error

This one ran ECHO HI - the method transformed the command text before it ran, not the output afterwards, and nothing reported it. Read a result first:

$`echo hi`.stdout.upper()

A Name That Isn't a Result

$`echo hi`.output         // Error: did you mean 'stdout'?

There are four results and no others. See rad docs RAD40025 for what each one gives you.

Why This Is an Error, Not a Warning

Every one of these shapes either did something other than what it reads like, or has no meaning at all. There's no version of them worth keeping working.

See Also

  • rad docs migrations/v0.12 - the full change and its rationale
  • rad docs RAD40025 - using a command as a value with no accessor
  • rad docs guide/shell-commands - the three command forms