Raku Behind the Docs All corners
Appendices · Appendix B

Writing a Probe

The ways a small test program measures something other than what its author meant, and how to write one whose output can be trusted.

20 corners · 41 examples

Every corner in this book began as a probe: a small program written to find out what Rakudo does. Probes are also how bugs are reported, how questions are asked on a forum, and how a test starts its life. A probe is only as good as its question, and Raku offers many ways for a probe to answer a different question from the one its author asked: an error that happens outside the try meant to catch it, a line the compiler computed before the program ran, an argument that changed its meaning, an output that changes from one run to the next.

Each corner below states one such trap, shows a probe that falls into it with the output that misleads, and then the corrected probe with the output that can be trusted. The rules behind most of the traps have corners of their own in the chapters, and the links lead there.

B.1 A try around a lazy Seq catches nothingTrap

A probe that asks whether some code throws usually wraps it in try and looks at $! afterwards. When the code returns a lazy Seq, as map does, none of it has run when the try ends. The elements are computed when something reads them, which is outside the try (The Sequence Operator). If nothing reads them, the error never shows at all:

my $r = try (1..3).map({ die "bad $_" if $_ == 2; $_ });
say $! ?? "threw: $!.message()" !! "did not throw";
Reference output
did not throw
The editor’s engine, Raku++, prints something else here
threw: bad 2

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

.eager computes every element on the spot, inside the try. So do .Array and assignment to an array. .List does not: it wraps the Seq in a list that is still computed as it is read, so it escapes in the same way.

my $r = try (1..3).map({ die "bad $_" if $_ == 2; $_ }).eager;
say $! ?? "threw: $!.message()" !! "did not throw";
my $l = try (1..3).map({ die "bad $_" if $_ == 2; $_ }).List;
say $! ?? "threw: $!.message()" !! "did not throw";
Reference output
threw: bad 2
did not throw
The editor’s engine, Raku++, prints something else here
threw: bad 2
threw: bad 2

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

B.2 Through try, a Failure looks like a die, or like successTrap

try turns on use fatal for its body, and under it a Failure returned by a call throws at once (Exceptions and Failures). A probe that uses try to learn how a routine reports an error therefore gets the same answer from a routine that dies and from one that fails:

sub thrower { die "no" }
sub failer  { fail "no" }
my $a = try thrower();
say "thrower: ", $!.^name;
my $b = try failer();
say "failer: ", $!.^name;
Reference output
thrower: X::AdHoc
failer: X::AdHoc

Without try the two are told apart: a CATCH sees what was thrown, and the result of the call shows what was returned. Asking the result .defined also marks the Failure handled, so it does not throw later.

sub thrower { die "no" }
sub failer  { fail "no" }
for &thrower, &failer -> &f {
    my $r = f();
    say &f.name, ": returned a ", $r.^name, ", defined: ", $r.defined;
    CATCH { default { say &f.name, ": threw ", .^name } }
}
Reference output
thrower: threw X::AdHoc
failer: returned a Failure, defined: False

The opposite happens with a comparison. == and < on a string that is not a number return a Failure that passes through the try without throwing (Numbers). The try counts as a success and resets $!, and only the value says what happened:

my $s = "abc";
my $r = try $s < 1;
say $! ?? "threw" !! "did not throw";
say $r.defined ?? "result: $r" !! "failed: " ~ $r.exception.^name;
Reference output
did not throw
failed: X::Str::Numeric
The editor’s engine, Raku++, prints something else here
threw

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

Test the result of the code under study, not $!.

B.3 $! belongs to the latest try in the routineTrap

A try that succeeds sets $! back to Any (Exceptions and Failures), and every try in a routine shares the routine's $!, even one in a nested block. A probe that catches the exception it studies, does a little more work and then prints $! prints the outcome of the work if the work contains a try:

try { die "the error under study" };
my $n = try { +"42" };
say $! ?? $!.message !! "no exception";
Reference output
no exception

Copy $! into a variable of its own straight after the try it belongs to:

try { die "the error under study" };
my $error = $!;
my $n = try { +"42" };
say $error ?? $error.message !! "no exception";
Reference output
the error under study

B.4 A try with a CATCH of its own lets the rest escapeTrap

A CATCH block inside a try replaces the handler that try would supply. Whatever no when or default in it matches leaves the try as if there were no try (Exceptions and Failures). A probe that runs several pieces of code and reports the exceptions its author expects ends at the first exception nobody expected, and the pieces after it never run:

my @probes = { die "plain" }, { +"abc" }, { 42 };
for @probes -> &code {
    try {
        code();
        CATCH { when X::AdHoc { say "caught: ", .^name } }
    }
}
say "all probes ran";
Reference output
caught: X::AdHoc
and on standard error
Cannot convert string to number: base-10 number must begin with valid digits or '.' in '<HERE>abc' (indicated by <HERE>)
  in block <unit> at example.raku line 1

Actually thrown at:
  in block  at example.raku line 4
  in block <unit> at example.raku line 2

Without the CATCH, try catches everything, and $! names what it caught:

my @probes = { die "plain" }, { +"abc" }, { 42 };
for @probes -> &code {
    try code();
    say $! ?? "caught: " ~ $!.^name !! "no exception";
}
say "all probes ran";
Reference output
caught: X::AdHoc
caught: X::Str::Numeric
no exception
all probes ran

B.5 Rakudo computes an operation on literals while it compilesTrap

When every operand of an operator is a literal, Rakudo may compute the result while it compiles the program and put the value in the operation's place. This is constant folding. When the program runs, there is no operation left, and nothing that should influence the operation reaches it. A probe that wraps the + operator to count its calls sees no call for 1 + 2:

my $calls = 0;
&infix:<+>.wrap(-> |c { $calls++; callsame });
my $sum = 1 + 2;
say "calls: $calls";
my $one = 1;
$sum = $one + 2;
say "calls: $calls";
Reference output
calls: 0
calls: 1
The editor’s engine, Raku++, prints something else here
calls: 0
calls: 0

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

A dynamic variable cannot reach a folded operation either: 1 / 2**64 becomes a Num however $*RAT-OVERFLOW is set when the line runs (Numbers). For the same reason, a benchmark of an expression made only of literals times nothing but a constant. An operation that throws or warns is not folded: Int + 1 still throws, and Nil == 0 still warns, when its line runs. A probe that takes its operands from variables sees the operation happen where it expects.

B.6 A literal mistake keeps the whole probe from compilingTrap

Some mistakes are visible in the source text: a literal of the wrong type for a typed variable, a literal negative index, a type parameterized with a value, a call whose literal argument can never match the signature. Rakudo refuses such a program before running any of it. A probe that asks several questions in one file then answers none of them, not even those on the lines before the mistake:

say "7 div 2 is ", 7 div 2;
sub greet(Str $name) { "hello $name" }
say greet(42);
Reference output
(nothing)
and on standard error
===SORRY!=== Error while compiling example.raku
Calling greet(Int) will never work with declared signature (Str $name)
at example.raku:3
------> say <HERE>greet(42);
The editor’s engine, Raku++, prints something else here
7 div 2 is 3

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

With the value in a variable, each mistake becomes an ordinary exception, or a Failure, when its line runs (Exceptions and Failures):

my $v = 1.5;
try { my int $x = $v };
say $!.^name;
my @a = 1, 2;
my $i = -1;
my $r = @a[$i];
say $r.defined ?? $r !! $r.exception.^name;
my $n = 42;
try { Array[$n] };
say $!.^name;
Reference output
X::AdHoc
X::OutOfRange
X::AdHoc
The editor’s engine, Raku++, prints something else here
Nil
X::OutOfRange
X::OutOfRange

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

To study the compile-time error itself, compile the code with EVAL, which turns the refusal into an exception that try can catch (Exceptions and Failures). One EVAL per question keeps each refusal from hiding the others:

use MONKEY-SEE-NO-EVAL;
for 'my int $x = 1.5', 'my @a = 1, 2; @a[-1]', 'Array[42]',
    'sub greet(Str $n) { }; greet(42)' -> $code {
    try EVAL $code;
    say $!.^name;
}
Reference output
X::Syntax::Number::LiteralType
X::Obsolete
X::AdHoc
X::TypeCheck::Argument+{X::Comp}
The editor’s engine, Raku++, prints something else here
X::Syntax::Number::LiteralType
X::Obsolete
X::OutOfRange
X::TypeCheck::Argument

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

B.7 Braces in a double-quoted EVAL string run before the EVALTrap

A probe often builds code in a string and hands it to EVAL. In a double-quoted string, { … } is a block that runs while the string is being built, and its value is interpolated (Quotes and Interpolation). EVAL then compiles something other than what was written:

use MONKEY-SEE-NO-EVAL;
say "say { 1 + 1 }.WHAT";
EVAL "say { 1 + 1 }.WHAT";
Reference output
say 2.WHAT
(Int)

Single quotes leave the braces alone, and EVAL compiles the code as written:

use MONKEY-SEE-NO-EVAL;
EVAL 'say { 1 + 1 }.WHAT';
Reference output
(Block)

A backslash inside the braces does not help, because the braces hold code, where a backslash is an operator and not an escape. A value that must go into the code is joined on with ~.

B.8 A list operator inside say takes every argument after itTrap

A sub called without parentheses, and a reduction such as [+], take the whole comma list that follows them (Whitespace, Precedence). Inside a say, that is every remaining argument, labels included. The result may be an error, or a plausible answer to the wrong question:

say "joined: ", join "-", 1, 2, 3, "!";
say "smallest: ", [min] 3, 1, 2, " (expected 1)";
Reference output
joined: 1-2-3-!
smallest:  (expected 1)

The label became one of the values compared. Compared with a number, a string is compared as a string, and one that starts with a space comes before every digit, so it is the smallest. Parentheses end the argument list where it was meant to end:

say "joined: ", join("-", 1, 2, 3), "!";
say "smallest: ", ([min] 3, 1, 2), " (expected 1)";
Reference output
joined: 1-2-3!
smallest: 1 (expected 1)

B.9 Precedence can build a different expression from the one intendedTrap

The parser reads a probe by the precedence table, not by its layout. When a probe dies of an error about something it did not ask, or earns a warning, it may be testing another expression. xx binds tighter than .., so this probe repeats the endpoint 3 rather than the range, and fails for a reason that has nothing to do with xx (Ranges):

my @r = 1..3 xx 2;
say @r;
Reference output
(nothing)
and on standard error
Seq objects are not valid endpoints for Ranges
  in block <unit> at example.raku line 1

Parentheses make the probe say what it means, and .raku shows what was built:

say ((1..3) xx 2).raku;
my $s = (1 ... 3);
say $s;
say (1..2) cmp (1..2);
Reference output
(1..3, 1..3).Seq
(1 2 3)
Same

Without its parentheses, my $s = 1 ... 3 is (my $s = 1) ... 3, which leaves 1 in $s and earns a warning (Whitespace). (1..2) cmp 1..2 does not compile at all, because cmp and .. share a non-associative level (Precedence).

B.10 A sub's $_, (* + 1).arity and try { $^a } misleadTrap

Three short spellings do not mean what a probe's author may take them for. A sub has a $_ of its own, which is neither its argument nor the caller's topic, and it starts out undefined. A method called on a * expression is added to the WhateverCode instead of asking it anything (Signatures):

sub double($n) { $_ * 2 }
say double(21);
say (* + 1).arity;
Reference output
0
WhateverCode.new
and on standard error
Use of uninitialized value $_ of type Any in numeric context
  in sub double at example.raku line 1

The third spelling does not compile. try followed by a block takes the block as its own body, and a try body cannot have placeholder parameters:

my $f = try { $^a * 2 };
Reference output
(nothing)
and on standard error
===SORRY!=== Error while compiling example.raku
Placeholder variable '$^a' may not be used here because the surrounding
block does not take a signature.
at example.raku:1
------> my $f = try { $^a * 2 }<HERE>;
    expecting any of:
        horizontal whitespace
        statement end
        statement modifier
        statement modifier loop
The editor’s engine, Raku++, prints something else here
(nothing: Raku++ accepts the program and prints nothing)

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

A parameter named $_ is the topic; a WhateverCode kept in a variable answers questions about itself; and parentheses make a block a value that try returns:

sub double($_) { $_ * 2 }
say double(21);
my $inc = * + 1;
say $inc.arity;
my $f = try ({ $^a * 2 });
say $f(21);
Reference output
42
1
42
The editor’s engine, Raku++, prints something else here
42
1
21

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

B.11 A Pair in a call is a named argument, not dataTrap

a => 1 or :a written directly in a call is a named argument (Signatures). A routine with no parameter of that name ignores it or refuses the call, so a probe that feeds Pairs to a routine may be feeding it nothing. And the left side of => is quoted when it is a bare word, so Int => 1 has the string "Int" as its key (Hashes):

my %h = a => 1;
%h.push(a => 2);
say %h;
say Bag.new(a => 3).elems;
my %o{Any} = Int => "type";
say %o.keys.map(*.^name);
Reference output
{a => 1}
0
(Str)

This probe concludes that push does not add to an existing key, that Bag.new drops a Pair, and that an object hash turns a type into a string. Parentheses make each Pair a value, and the answers change:

my %h = a => 1;
%h.push((a => 2));
say %h;
say Bag.new((a => 3)).elems;
my %o{Any} = (Int) => "type";
say %o.keys.map(*.^name);
Reference output
{a => [1 2]}
1
(Int)

push stacks the new value on the old one (Hashes), and Bag.new makes the Pair an element (Sets, Bags and Mixes). The subs set and bag refuse a named argument outright: set(:a) dies with X::Multi::NoMatch.

B.12 say prints nothing if one argument cannot be printedTrap

say turns all its arguments into text before it prints any of them. When one cannot be turned into text, say dies, and the values before it are lost with it. A probe that prints several results on one line reports only the error, which then seems to be about the whole line:

my ($a, $b) = 1, 0;
say "sum: ", $a + $b, ", ratio: ", $a / $b;
Reference output
(nothing)
and on standard error
Attempt to divide 1 by zero when coercing Rational to Str
  in block <unit> at example.raku line 2

The division succeeded. 1 / 0 is a Rat, and only its conversion to text fails (Numbers). An unhandled Failure among the arguments, such as +"x", takes the line down in the same way. One result per say keeps the good ones, and .raku shows a value that has no text form:

my ($a, $b) = 1, 0;
say "sum: ", $a + $b;
say "ratio: ", ($a / $b).raku;
Reference output
sum: 1
ratio: <1/0>

B.13 Plain printing hides what a value isTrap

put and print show a value's .Str, and say its .gist. Both forms are made for people, and both drop what a probe often needs to know. Under put, the empty string, Nil and an undefined Any all print as an empty line, two of them with a warning (Nil, Any and the Undefined). Under say, a number and a string of the same digits look alike:

put "";
put Nil;
put Any;
say 1, " ", "1";
Reference output


1 1
and on standard error
Use of Nil in string context
  in block <unit> at example.raku line 2
Use of uninitialized value of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to something meaningful.
  in block <unit> at example.raku line 3

.raku tells each of them apart, silently:

say "".raku;
say Nil.raku;
say Any.raku;
say 1.raku, " ", "1".raku;
Reference output
""
Nil
Any
1 "1"

B.14 Hash and set order changes with every runTrap

Rakudo walks the pairs of a hash in an order that differs from one process to the next (Hashes, Maps and Pairs). A probe that prints .keys, .values, .pairs or .kv, or the result of .fmt or .map on a hash, shows one order on one run and another on the next, and the order it happened to see proves nothing. The same holds for the elements of a Set or a Bag listed by .keys or .Str, and for the named captures of a match listed by .keys (Regexes):

my %h = a => 1, b => 2, c => 3, d => 4;
say %h.keys;
say %h.fmt("%s=%s", ",");
say set(<a b c d>).keys;
not run

Three runs of this program began with (c a b d), (b d c a) and (a b c d); the third order happened to be sorted, which proves nothing either. Sort before printing. The gist, .Str and .raku of a whole Hash are sorted already, and so is the gist of a Set or a Bag (Sets, Bags and Mixes):

my %h = a => 1, b => 2, c => 3, d => 4;
say %h.keys.sort;
say %h.sort.map(*.fmt("%s=%s")).join(",");
say set(<a b c d>).keys.sort;
say %h;
"x1" ~~ / $<letter>=(\w) $<digit>=(\d) /;
say $/.keys.sort;
Reference output
(a b c d)
a=1,b=2,c=3,d=4
(a b c d)
{a => 1, b => 2, c => 3, d => 4}
(digit letter)

B.15 Several "Useless use" warnings come out in a random orderQuirk

The compiler collects its reports of useless values (Values Nobody Uses) and prints them in one block after compiling. When there are several, their order changes from one run to the next:

my $x = 1;
$x + 1;
$x * 2;
42;
say $x;
not run

The three reports of this probe came out in a different order on almost every run: line 2 first on one run, line 4 or line 3 first on others. A probe that studies these warnings provokes one per program:

my $x = 1;
$x * 2;
say $x;
Reference output
1
and on standard error
WARNINGS for example.raku:
Useless use of "*" in expression "$x * 2" in sink context (line 2)

B.16 After srand, a first pass draws other numbersQuirk

srand seeds the generator behind rand, pick and roll, so that the same seed should give the same numbers. In Rakudo 2026.08 the same seed gives the same numbers only when the code that draws them has already run once after a seed (Numbers). A probe that seeds twice and compares the draws concludes that srand repeats nothing:

srand(42); my @first = (^100).roll(5);
srand(42); my @second = (^100).roll(5);
say @first eqv @second;
Reference output
False
The editor’s engine, Raku++, prints something else here
True

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

Run the same comparison twice, and the second pass gives the answer that holds from then on:

for 1, 2 -> $pass {
    srand(42); my @first = (^100).roll(5);
    srand(42); my @second = (^100).roll(5);
    say "pass $pass: ", @first eqv @second;
}
Reference output
pass 1: False
pass 2: True
The editor’s engine, Raku++, prints something else here
pass 1: True
pass 2: True

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

The code that counts includes Rakudo's own. Two ways of reading the same roll draw different numbers after the same seed, on every pass: the gist of the Seq and an array assigned from it do not agree.

for 1, 2 {
    srand(42); my $shown = (^100).roll(5).gist;
    srand(42); my @kept = (^100).roll(5);
    say $shown eq @kept.List.gist;
}
Reference output
False
False
The editor’s engine, Raku++, prints something else here
True
True

Measured with Raku++ 4.0.1-245-ge1e6e3a1-modified (2026-09-28) arm64-darwin. The book shows the reference compiler’s output; the editor runs Raku++ compiled to WebAssembly.

The numbers are the same from one run of the program to the next, so a probe that prints them is repeatable. It is not evidence of what another version of Rakudo, or the same code reached another way, will draw.

B.17 A dropped Failure warns at an unpredictable momentTrap

A Failure that is never tested is reported when the garbage collector frees it (Exceptions and Failures). Looking at its type with .^name, or at its message, does not count as a test (Values Nobody Uses). A probe that makes Failures in a loop and examines each one prints warnings whose number depends on when the collector happens to run:

sub lookup($k) { fail "no key $k" }
for ^5000 { my $f = lookup($_); $f.^name }
say "done";
not run

Four runs of this probe printed 4,852, 2,265, 2,267 and 4,450 warnings on standard error, each beginning WARNING: unhandled Failure detected in DESTROY. A probe with a single Failure usually prints none, which is no better: the same probe, grown a little, may start to warn. A probe tests every Failure it creates, with .so, .defined or a Boolean context, and the collector has nothing to report:

sub lookup($k) { fail "no key $k" }
my $missing = 0;
for ^5000 { my $f = lookup($_); $missing++ unless $f }
say $missing;
Reference output
5000

B.18 A probe that prints a path prints where it ranTrap

$*CWD, the result of .absolute and $*EXECUTABLE name places on the machine that runs the probe. Printed, they make the output differ from one directory or one machine to the next, and they say nothing about the behaviour under study. $*EXECUTABLE is the binary's path with symbolic links followed, which need not be the path that was typed to run it:

say $*CWD;
say "a.txt".IO.absolute;
say $*EXECUTABLE;
not run

Print a test of the property the probe is about instead:

say "a.txt".IO.absolute eq $*CWD.add("a.txt").Str;
say "a.txt".IO.absolute.IO.is-absolute;
say $*EXECUTABLE.is-absolute;
say $*EXECUTABLE.e;
run it locally
Reference output
True
True
True
True

B.19 An endless list hangs a probe that has no timerTrap

.eager computes every element of a list, and on an endless list it never finishes. .elems of a lazy Range or Array returns a Failure (Lists, Arrays, Seqs and Slips), but a gather does not count as lazy (Lists), and .elems of an endless one runs for ever too. Each of these lines hangs on its own:

say (1..*).eager.elems;
say (1, 2 ... *).eager.elems;
say (gather { my $i = 0; loop { take $i++ } }).elems;
not run

A hanging probe blocks whatever runs it, a terminal or a test suite. Run every probe under a timer. With Perl, which is present on almost every Unix system, that takes one line:

perl -e 'alarm 10; exec @ARGV' rakudo probe.raku

After ten seconds the alarm signal ends the process. What the probe printed before the hang stays; the line that hangs prints nothing, and the exit status is 142, which is 128 plus the signal number 14. A probe that shows no output at all may have hung on its first line, rather than printed an empty result.

B.20 An output that changes between runs cannot be verified

This book runs every example twice. The build keeps the reference output of each example; with --fresh it runs every example again and fails if any prints something different. An example whose output changes from run to run cannot be checked, because no single text is right, and the traps above are where such outputs come from. A probe can check itself the same way, by running its code in two separate processes and comparing:

my $probe = 'my %h = <a b c d e f> Z=> 1..6; print %h.keys.sort';
my @out = (1, 2).map: { run($*EXECUTABLE, "-e", $probe, :out).out.slurp(:close) };
say @out[0] eq @out[1];
say @out[0];
run it locally
Reference output
True
a b c d e f

Without .sort, five runs of the same code printed five different orders. Before trusting what a probe prints:

  • reify a lazy result inside the try that guards it;
  • test the value the code returns, not $!, and copy $! at once;
  • take operands from variables, and put code for EVAL in single quotes;
  • parenthesise a list operator or a reduction inside say;
  • print one result per say, with .raku when the type matters;
  • sort hash keys, set elements and capture names;
  • provoke one "Useless use" warning per program, and test every Failure;
  • compare seeded random draws only after a first pass;
  • print a test instead of a path;
  • run the probe under a timer, and run it twice.