This is part six in the "Cases of UPPER" series of blog posts, describing the Raku syntax elements that are completely in UPPERCASE.
This part will discuss the phasers that catch exceptions of various kinds.
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 it 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 $_.
This is the reason you cannot have a
CATCHthunk: it needs to have a scope to be able to set$_in there, without affecting anything outside of that scope.
It does not matter where in a scope you put the CATCH block. But it is recommended for clarity's sake to put a CATCH block as early in the scope as possible (rather than "hiding" it somewhere near the end of a scope): when reading the code, you will almost immediately see that there is something special going on with regards 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 will show:
naughty: urgh
alive still
Note that having the exception in $_ smart-matching with when effectively disables the exception so it won't be re-thrown on scope exit. Because of that, the say "alive still" will be executed. Any other type of error would not be disabled, although you could if you wanted do that with a default block.
The careful reader will have noticed that the say "after" was not executed. That's because the current scope was left as if a return Nil was executed in the scope where the CATCH block is located.
If you feel like the exception in question is benign, you can make execution continue in the statement following the one that caused the exception. You can do this by calling the .resume method on the exception object.
So let's assume all errors we get in that block are benign, and we want to just continue after each exception:
{
CATCH { .resume }
die "urgh";
say "after";
}
say "alive still";
would show:
after
alive still
However, not all exceptions are resumable! So your program may stop nonetheless if the exception can not be actually resumed. For instance, division by 0 errors are not resumable:
CATCH { .resume }
say 1/0;
would show:
This exception is not resumable
in block foo at bar line 42
Trying code
The try statements prefix (which either takes a thunk or a block) is actually a simplified case of a CATCH handler that disarms all errors, sets $! and returns Nil. The code:
say try die "urgh"; # Nil
dd $!; # $! = X::AdHoc.new(payload => "urgh")
is actually pretty much short for:
say {
CATCH {
CALLERS::<$!> = $_; # set $! in right scope
default { } # disarm exception
}
die "urgh";
}(); # Nil
dd $!; # $! = X::AdHoc.new(payload => "urgh")
Note that even though die "urgh" is a thunk, the compiler turned this into its own scope internally to be able to handle the return from the thunk.
CONTROL
Apart from runtime exceptions, many other types of exceptions are used in Raku. They all consume the X::Control role and are therefore referred to as "control exceptions". They are used for the following Raku features (in alphabetical order):
-
done- call "done" callback on all taps -
emit- send item to all taps of supply -
last- exit the loop structure -
next- start next iteration in loop structure -
proceed- resume aftergivenblock -
redo- restart iteration in loop structure -
return- return from sub / method -
succeed- exitgivenblock -
take- pass item togather -
warn- warn with given message
Just as runtime exceptions they all perform the work they are expected to do without needing any specific action from a user. However, you can not catch these exceptions with a CATCH phaser, you need a CONTROL phaser for that.
Handling control exceptions yourself
Suppose you have a pesky warning that you want to get rid of:
say $_ ~ "foo";
which would show:
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
Since warnings are control exceptions, you can "catch" them in a CONTROL phaser like this:
CONTROL {
when CX::Warn { .resume }
}
say $_ ~ "foo";
The class names for these control exceptions can be determined from title casing the name of the feature, and then prefixing with
CX::. So "warn" throws an instance of theCX::Warnclass.
which would just show:
foo
Pretty simple, eh? Well, by popular demand there is actually a shortcut for this case, called the quietly statement prefix:
quietly say $_ ~ "foo";
The standard way of handling warnings only produces the exact call-site where the warning occurred. Which sometimes is just not enough to find the reason for the warning, because you would need a full stack trace for that. Well, you can do that as well with a CONTROL block:
CONTROL {
when CX::Warn {
note .message;
note .backtrace.join;
.resume;
}
}
say $_ ~ "foo"
would show:
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
Or if you would like to turn any warning into a runtime exception:
CONTROL {
when CX::Warn { .throw }
}
say $_ ~ "foo";
would show:
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
and not show the "foo" because it would never get to that.
Building your own control exceptions
Can you build you own control exceptions? Yes, you can. Are they useful? Probably not. But just in case someone finds a way to make this useful, here's how you do it. You start with a class that consumes the X::Control role:
class Frobnicate does X::Control {
has $.message;
}
Then you create a nice subroutine to make it easier to create an instance of that class, pass it any arguments and throw the instantiated control exception object:
sub frobnicate($message) {
Frobnicate.new(:$message).throw
}
Then in your code, make sure there is a CONTROL phaser in the dynamic scope that handles the control exception:
CONTROL {
when Frobnicate {
say "Caught a frobnication: $_.message()";
.resume;
}
}
And then execute the nice subroutine at will:
say "before";
frobnicate "This";
say "after";
which would show:
before
Caught a frobnication: This
after
Final notes
Even though the CATCH and CONTROL phasers are scope based, you can not introspect a given Block to see whether it has CATCH or CONTROL phasers. This is because you can only have one of them in a scope, and handling exceptions in these phasers actually needs to be built into the bytecode generated for a block. So from a runtime point of view, there was no need to put them as attributes into the Block object.
Exception handling in Raku is built on delimited continuations. If you really want to get into the nitty gritty of this feature, you might be interested in reading "Continuations in NQP".
Conclusion
The CATCH phaser catches exceptions that are supposed to be fatal. The CONTROL phasers catches exceptions for all sorts of "normal" functionality, such as next, last, warn, etc.
You can build your own control exceptions, but the utility of these remains unclear as of yet.
Exceptions are built on top of so-called "delimited continuations".
This concludes the sixth episode of cases of UPPER language elements in the Raku Programming Language. Stay tuned for more!
Top comments (0)