Exceptions
Exceptions in Raku are objects that hold information about errors. An error can be, for example, the unexpected receiving of data or a network connection no longer available, or a missing file. The information that an exception object stores is, for instance, a human-readable message about the error condition, the backtrace of the raising of the error, and so on.
All built-in exceptions inherit from Exception, which provides some basic behavior, including the storage of a backtrace and an interface for the backtrace printer.
Typed exceptions
Typed exceptions are intended for use to indicate any of several predefined error conditions. Each of these provides information about the error, and when thrown may be caught by code intended to handle that kind of error. Exceptions can be delayed, that is, generated for handling but not thrown, by wrapping them in a Failure object.
For example, if while executing .frobnicate on an object, a needed path
foo/bar becomes unavailable, then an
X::IO::DoesNotExist exception can be thrown:
method frobnicate($path) {
X::IO::DoesNotExist.new(:$path, :trying("frobnicate")).throw
unless $path.IO.e;
# do the actual frobnication
}
frobnicate("foo/bar");
# OUTPUT: «Failed to find 'foo/bar' while trying to do '.frobnicate'
# in block <unit> at my-script.raku:1Ā»
Note how the object has provided the backtrace with information about what went wrong. A user of the code can now more easily find and correct the problem.
Instead of calling the .throw method on the X::IO::DoesNotExist
object, one can also use that object as a parameter to die:
die X::IO::DoesNotExist.new(:$path, :trying("frobnicate"));
Ad hoc exceptions
If you call .throw, die, or fail on an argument that is not some type of Exception, then that argument gets wrapped in an X::AdHoc object which is then used by the call.
For example, you can call die
on a string describing the error, and that string will become the
message of the X::AdHoc exception:
die "oops, something went wrong";
# OUTPUT: Ā«oops, something went wrong in block <unit> at my-script.raku:1ā¤Ā»
It is worth noting that die prints the error message to the standard error
$*ERR.
X::AdHoc is an Exception type like any other, but the intent is for use as a default type; it should not be used in cases for which specific Exception types have already been provided. It's probably not helpful to have a specific catch handler for the type of X::AdHoc.
Catching exceptions
It's possible to handle exceptional circumstances by supplying a CATCH block:
CATCH {
when X::IO { $*ERR.say: "some kind of IO exception was caught!" }
}
X::IO::DoesNotExist.new(:$path, :trying("frobnicate")).throw
# OUTPUT: «some kind of IO exception was caught!»
Here, we are saying that if any exception of type X::IO occurs, then the
message some kind of IO exception was caught! will be sent to stderr,
which is what $*ERR.say does, getting displayed on whatever constitutes the
standard error device in that moment, which will probably be the console by
default.
Note that the match target is a role. To allow user defined exceptions
to match in the same manner, they must implement the given role. Just
existing in the same namespace will make them look alike but won't match
in a CATCH block.
A CATCH block places any exception thrown in its topic variable
($_), thus it's possible to catch and handle various categories of
exceptions inside a when block. To handle all exceptions, use a
default statement. This example prints out almost the same
information as the normal backtrace printer. Note that the dot
methods apply to $_, which holds the
Exception within the CATCH block.
CATCH {
default {
$*ERR.say: .message;
for .backtrace.reverse {
next if .file.starts-with('SETTING::');
next unless .subname;
$*ERR.say: " in block {.subname} at {.file} line {.line}";
}
}
}
While this is a very common pattern, it is not strictly necessary to use
default or when in a CATCH block. This is done to prevent the
control flow from reaching the end of the block where, unless the
exception has been resumed, the exception
will continue to be thrown. Allowing this can be used for logging
purposes, for instance:
# In the outermost block of a script...
my IO::Handle:D $log = open sprintf('logs/%d-%d.txt', $*INIT-INSTANT, $*PID), :a;
CATCH { $log.printf: "[%d] Died with %s: %s$?NL", now, .^name, .message }
END { $log.close }
The CATCH block semantics apply to the entire lexical scope
in which it is defined, regardless of where it is defined inside that
lexical scope. It is therefore advised to put any CATCH block at the
start of the lexical scope to which they apply so that the casual reader
of the code can immediately see that there is something special going on.
Exception handlers and enclosing blocks
After a CATCH has handled the exception, the block enclosing the CATCH block
is exited.
In other words, even when the exception is handled successfully, the rest of the code in the enclosing block will never be executed.
die "something went wrong ...";
CATCH {
# will definitely catch all the exception
default { .Str.say; }
}
say "This won't be said."; # but this line will be never reached since
# the enclosing block will be exited immediately
# OUTPUT: Ā«something went wrong ...ā¤Ā»
Compare with this:
{
CATCH {
default { .Str.say; }
}
die "something went wrong ...";
}
say "Hi! I am at the outer block!"; # OUTPUT: Ā«Hi! I am at the outer block!ā¤Ā»
See Resuming of exceptions, for how to return control back to where the exception originated.
try blocks|Language,try blocks
A try block is a normal block which implicitly turns on the
use fatal pragma and
includes an implicit CATCH block that drops the exception, which
means you can use it to contain them. Caught exceptions are stored
inside the $! variable, which holds a value of type Exception.
(Note that immediately after a try block, if no Exception
had been thrown, then $! will be undefined.)
A normal block like this one will simply fail:
{
my $x = +"a";
say $x.^name;
} # OUTPUT: Ā«Failureā¤Ā»
However, a try block will contain the exception and put it into the
$! variable:
try {
my $x = +"a";
say $x.^name;
}
if $! { say "Something failed!" } # OUTPUT: Ā«Something failed!ā¤Ā»
say $!.^name; # OUTPUT: Ā«X::Str::Numericā¤Ā»
Any exception that is thrown in such a block will be caught by a
CATCH block, either implicit or provided by the user. In the latter
case, any unhandled exception will be rethrown. If you choose not to
handle the exception, they will be contained by the block.
try {
die "Tough luck";
say "Not gonna happen";
}
try {
fail "FUBAR";
}
In both try blocks above, exceptions will be contained within the
block, but the say statement will not be run. We can handle them,
though:
class E is Exception { method message() { "Just stop already!" } }
try {
E.new.throw; # this will be local
say "This won't be said.";
}
say "I'm alive!";
try {
# NOTE: Raku allows you to place the CATCH block anywhere in the
# scope of the enclosing block. Some prefer it at the top for
# visibility, others at the end.
CATCH {
when X::AdHoc { .Str.say; .resume }
}
die "No, I expect you to DIE Mr. Bond!";
say "I'm immortal.";
E.new.throw;
say "No, you don't!";
}
Which would output:
I'm alive!
No, I expect you to DIE Mr. Bond!
I'm immortal.
Just stop already!
in block <unit> at exception.raku line 21
This is because the CATCH block is handling just the X::AdHoc exception
thrown by the die statement, but not the E exception. In the
absence of a CATCH block, all exceptions will be contained and
dropped, as indicated above. resume will resume execution right after
the exception has been thrown; in this case, in the die statement.
Please consult the section on
resuming of exceptions
for more information on this.
A try block is a normal block and as such, uses its last statement
as its return value. We can therefore use it as a right-hand
side.
say try { +"99999" } // "oh no"; # OUTPUT: Ā«99999ā¤Ā»
say try { +"hello" } // "oh no"; # OUTPUT: Ā«oh noā¤Ā»
Try blocks support else blocks indirectly by returning the return
value of the expression or Nil if an exception was thrown.
with try +"ā„" {
say "this is my number: $_"
} else {
say "not my number!"
}
# OUTPUT: Ā«not my number!ā¤Ā»
try can also be used with a statement instead of a block, that is, as a
statement prefix:
say try "some-filename.txt".IO.slurp // "sane default";
# OUTPUT: Ā«sane defaultā¤Ā»
What try actually causes is, via the use fatal pragma, an immediate throw
of the exceptions that happen within its scope, but by doing so the CATCH
block is invoked from the point where the exception is thrown, which defines its
scope.
my $error-code = "333";
sub bad-sub {
die "Something bad happened";
}
try {
my $error-code = "111";
bad-sub;
CATCH {
default {
say "Error $error-code ", .^name, ': ',.Str
}
}
}
# OUTPUT: Ā«Error 111 X::AdHoc: Something bad happenedā¤Ā»
Throwing exceptions
Exceptions can be thrown explicitly with the die routine and with the .throw method of an Exception object. In addition, unhandled Failure objects will throw when garbage-collected.
This example throws an X::AdHoc exception, catches it and allows the code
to continue from the point of the exception by calling the .resume method.
{
X::AdHoc.new(:payload<foo>).throw;
"OHAI".say;
CATCH {
when X::AdHoc { .resume }
}
}
"OBAI".say;
# OUTPUT: Ā«OHAIā¤OBAIā¤Ā»
If the CATCH block doesn't match the exception thrown, then the
exception's payload is passed on to the backtrace printing mechanism.
{
X::AdHoc.new(:payload<foo>).throw;
"OHAI".say;
CATCH { }
}
"OBAI".say;
# OUTPUT: «foo
# in block <unit> at my-script.raku:1Ā»
This next example doesn't resume from the point of the exception. Instead,
it continues after the enclosing block, since the exception is caught, and then
control continues after the CATCH block.
{
X::AdHoc.new(:payload<foo>).throw;
"OHAI".say;
CATCH {
when X::AdHoc { }
}
}
"OBAI".say;
# OUTPUT: Ā«OBAIā¤Ā»
throw can be viewed as the method form of die, just that in this
particular case, the sub and method forms of the routine have different
names.
Delaying exceptions as failures
As noted above in Ad hoc exceptions, in addition to throw and die, you can also call fail on an exception in order to handle it later. This is a usual thing to do if you want to return the exception to the caller for them to handle.
If the resulting Failure object is returned into sink
context (that is, not handled), it's the same as if you had called die or
throw. If it is instead returned into Boolean context or checked for
definedness, then it is considered "handled" and does not throw. If it is
assigned to a variable, the throw is delayed until the variable is
garbage-collected if it hasn't been handled before that time.
For example, assuming you don't have permission to create a new directory at
the root level, then mkdir "/test" will return a Failure:
mkdir "/test"; # Returns Failure into sink context, acts like die()
my $result = mkdir "/test"; # throws when garbage-collected unless handled by then
so mkdir "/test"; # Failure "handled", doesn't die
if mkdir "/test" { # Failure also "handled", doesn't die
... # mkdir was successful
}
unless mkdir "/test" {
... # Here we can handle the Failure (for real)
}
Resuming of exceptions
Exceptions interrupt control flow and divert it away from the statement
following the statement that threw it. Any exception handled by the
user can be resumed and control flow will continue with the statement
following the statement that threw the exception. To do so, call the
method .resume on the exception object.
CATCH { when X::AdHoc { .resume } } # this is step 2
die "We leave control after this."; # this is step 1
say "We have continued with control flow."; # this is step 3
Resuming will occur right after the statement that has caused the exception, and in the innermost call frame:
sub bad-sub {
die "Something bad happened";
return "not returning";
}
{
my $return = bad-sub;
say "Returned $return";
CATCH {
default {
say "Error ", .^name, ': ', .Str;
$return = '0';
.resume;
}
}
}
# OUTPUT:
# Error X::AdHoc: Something bad happened
# Returned not returning
In this case, .resume is getting to the return statement that happens
right after the die statement. Please note that the assignment to $return
is taking no effect, since the CATCH statement is happening inside the
call to bad-sub, which, via the return statement, assigns the not
returning value to it.
Uncaught exceptions
If an exception is thrown and not caught, it causes the program to exit with a
non-zero status code, and typically prints a message to the standard error
stream of the program. This message is obtained by calling the gist method
on the exception object. You can use this to suppress the default behavior of
printing a backtrace along with the message:
class X::WithoutLineNumber is X::AdHoc {
multi method gist(X::WithoutLineNumber:D:) {
$.payload
}
}
die X::WithoutLineNumber.new(payload => "message")
# prints "message\n" to $*ERR and exits, no backtrace
Control exceptions
Control exceptions are raised when throwing an Exception which does the X::Control role (since Rakudo 2019.03). They are usually thrown by certain keywords and are handled either automatically or by the appropriate phaser. Any unhandled control exception is converted to a normal exception.
{ return; CATCH { default { $*ERR.say: .^name, ': ', .Str } } }
# OUTPUT: Ā«X::ControlFlow::Return: Attempt to return outside of any Routineā¤Ā»
# was CX::Return