Catch Control
Source: Dev.to
Cases of UPPER – Part 6: Phasers that Catch Exceptions
CATCH
Many programming languages have a try / catch mechanism. Although it is true that the Raku Programming Language does have a try statement prefix and does have a CATCH phaser, you should generally not use both at the same time.
In Raku any scope can have a single CATCH block. The code within it will be executed as soon as any runtime exception occurs in that scope, with the exception that was thrown topicalized in $_ (see docs).
- This is why you cannot have a
CATCHthunk: it needs a scope in order to set$_without affecting anything outside that scope. - It does not matter where in a scope you put the
CATCHblock, but for clarity it is recommended to place it as early as possible (rather than “hiding” it near the end of a scope). When reading the code you will immediately see that something special is happening with respect to exceptions.
Handling exceptions
Let’s start again with a contrived example:
{
CATCH {
when X::AdHoc {
say "naughty: $_.message()";
}
}
die "urgh"; # throws an X::AdHoc exception
say "after";
}
say "alive still";
Running the above code prints:
naughty: urgh
alive still
Note: Matching the exception in $_ with when effectively disables the exception, so it won’t be re‑thrown on scope exit. Because of that, say "alive still" is executed. Any other type of error would not be disabled, although you could do that with a default block (see docs).
The careful reader will have noticed that say "after" was not executed. That’s because the current scope was left as if a return Nil had been executed in the scope where the CATCH block resides.
If the exception is benign, you can make execution continue with the statement following the one that caused the exception by calling the .resume method on the exception object:
{
CATCH { .resume }
die "urgh";
say "after";
}
say "alive still";
Output:
after
alive still
Important: Not all exceptions are resumable! For example, division by
0errors are not resumable:
CATCH { .resume }
say 1/0;
Output (example):
This exception is not resumable
in block foo at bar line 42
Trying code
The try statement prefix (which takes either a thunk or a block) is essentially a simplified CATCH handler that disarms all errors, sets $!, and returns Nil.
say try die "urgh"; # Nil
dd $!; # $! = X::AdHoc.new(payload => "urgh")
is roughly equivalent to:
say {
CATCH {
CALLERS:: = $_; # set $! in the right scope
default { } # disarm exception
}
die "urgh";
}(); # Nil
dd $!; # $! = X::AdHoc.new(payload => "urgh")
Even though die "urgh" is a thunk, the compiler creates its own scope internally to handle the return from the thunk.
CONTROL
Besides runtime exceptions, many other types of exceptions are used in Raku. They all consume the X::Control role (docs) and are therefore called control exceptions (see language guide). They are used for the following Raku features (alphabetical order):
| Control exception | Description |
|---|---|
done | Call “done” callback on all taps |
emit | Send item to all taps of a supply |
last | Exit the loop structure |
next | Start next iteration in a loop |
proceed | Resume after a given block |
redo | Restart iteration in a loop |
return | Return from a sub/method |
succeed | Exit a given block |
take | Pass item to gather |
warn | Emit a warning with a given message |
Just like runtime exceptions, control exceptions perform their intended work automatically. However, you cannot catch them with a CATCH phaser; you need a CONTROL phaser (docs).
Handling control exceptions yourself
Suppose you have a pesky warning that you… (the original text ends here).
Getting Rid of the Warning Output
You may want to suppress the warning that appears when you run:
say $_ ~ "foo";
The warning looks like this:
Use of uninitialized value element of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to something meaningful.
in block foo at bar line 42
foo
Using a CONTROL Phaser
Warnings are control exceptions, so you can catch them with a CONTROL phaser:
CONTROL {
when CX::Warn { .resume }
}
say $_ ~ "foo";
Now only the desired output is shown:
foo
The quietly Statement Prefix
Raku also provides a shortcut for this case: the quietly statement prefix.
quietly say $_ ~ "foo";
Getting a Full Stack Trace for Warnings
The default handling of warnings only shows the call‑site where the warning occurred.
If you need a full backtrace, use a CONTROL block that prints the message and the backtrace:
CONTROL {
when CX::Warn {
note .message;
note .backtrace.join;
.resume;
}
}
say $_ ~ "foo";
Output example:
Use of uninitialized value element of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to something meaningful.
in sub warn at SETTING::src/core.c/control.rakumod line 267
in method Str at SETTING::src/core.c/Mu.rakumod line 817
in method join at SETTING::src/core.c/List.rakumod line 1200
in sub infix: at SETTING::src/core.c/Str.rakumod line 3995
in block foo at bar line 42
Turning a Warning into a Runtime Exception
If you prefer a warning to abort execution, re‑throw it as an exception:
CONTROL {
when CX::Warn { .throw }
}
say $_ ~ "foo";
Result:
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 foo at bar line 42
The "foo" line never appears because execution stops at the thrown exception.
Building Your Own Control Exceptions
Defining the Exception Class
class Frobnicate does X::Control {
has $.message;
}
Helper Subroutine to Throw It
sub frobnicate($message) {
Frobnicate.new(:$message).throw
}
Handling the Custom Exception
CONTROL {
when Frobnicate {
say "Caught a frobnication: $_.message()";
.resume;
}
}
Using the Subroutine
say "before";
frobnicate "This";
say "after";
Output:
before
Caught a frobnication: This
after
Final Notes
CATCHandCONTROLphasers are scope‑based; you cannot introspect aBlockat runtime to discover whether it contains aCATCHorCONTROLphaser.- Only one of these phasers may exist in a given scope, and handling is baked into the block’s bytecode.
- Exception handling in Raku is built on delimited continuations. For a deep dive, see the article “Continuations in NQP”.
Conclusion
CATCHhandles fatal exceptions.CONTROLhandles “normal” control flow exceptions such asnext,last,warn, etc.- You can create your own control exceptions, though their practical utility may be limited.
- All exception handling in Raku relies on delimited continuations.
This concludes the sixth episode of “Cases of UPPER Language Elements” in the Raku Programming Language. Stay tuned for more!