Raku by Example: Exceptions

Exceptions in Raku are objects that hold information about errors. As objects, these exceptions are handled by the Exception class and its derived classes.

The try block

The try block is one way of handling exceptions in Raku. The code that produces the error is surrounded by a try block and regardless of whether an exception occurs, the code following the try block will be executed. If the exception happens, then it's stored in the error variable, $!, which can later be used.

say "Before division by zero!";   # This is executed
try {
    say "Result: ", 3 / 0;        # "Result: " is not printed
}
say "After division by zero!";    # This is executed too
say "Error: ", $!.Str;            # Printing the content of `$!`
$ perl6 error.p6
Before division by zero!
After division by zero!
Error: Attempt to divide 3 by zero using div

When dealing with an exception, the place where the exception is used determines when it's thrown. For example, the following attempt to catch an exception with a try block will fail to do the expected job:

say "Before division by zero!";
say divide(3, 0);
say "After division by zero!";

sub divide($a, $b) {
    my $result;
    try {
        $result = $a / $b
    }
    return $result;
}
$ perl6 error.p6
Before division by zero!
Attempt to divide 3 by zero using div
  in block <unit> at RAKU-QUICK-PROTO.p6 line 2

The exception wasn't handled properly because surrounding the operation where a division by zero might occur inside the sub with the try block is not enough. The exception doesn't happen inside the sub definition so the $! variable doesn't contain the Exception object but the Any object. However the following achieves the goal:

say "Before division by zero!";
try {
    say "Result: ", divide(3, 0);  # The result of a division by zero is
                                   # about to be used here.
}
say "After division by zero!";

sub divide($a, $b) {
    my $result;
    $result = $a / $b; # A division by zero might occur but it's not important
    return $result;    # to catch the error here. It should be caught where
                       # the result of such division is about to be used.
}
$ perl6 error.p6
Before division by zero!
After division by zero!

Similarly:

say "Before any division by zero!";
my $result = divide(3, 0);      # division occurs here but...      
say "Before result is used!";   # ... this still prints.
try {
    say "Result:", $result;     # exception is thrown here because `$result`
                                # is being used. And then captured by the
                                # `try` block
}
say "After result is used!";

sub divide($a, $b) {
    my $result;
    $result = $a / $b;
    return $result;
}
$ perl6 error.p6
Before any division by zero!
Before result is used!
After result is used!

These last two code snippets illustrate the mechanism of failing softly allowed by Raku. In short, this means that exceptions aren't thrown until the value associated with them is about to be used. And this is what happens in these code snippets; the exceptions are only thrown when the value from divide(3,0) is used.

The CATCH phaser

Instead of a try block, the CATCH block might be used to handle exceptions. A CATCH block is triggered as soon as an exception occurs in the current block and hasn't been handled yet (with a try block, for example).

my $result = divide(3, 0);        # division occurs here but...      

say "Dividing by zero!";

say "Result:", $result;           # ... exception is thrown here

CATCH {                           # `CATCH` was triggered so its statement
    say "==> Exception caught!"   # is executed. However the exception is
                                  # not handled yet and its message will still
                                  # printed.
}

sub divide($a, $b) {
    my $result;
    $result = $a / $b;
    return $result;
}
$ perl6 catch_block.p6
Dividing by zero!
==> Exception caught!
Attempt to divide 3 by zero using div
  in block <unit> at catch_block.p6 line 2

As mentioned previously, exceptions are handled via the objects of the Exception class and its derived classes. This means that we can introspect an exception and gather information important from it.

my $result = 1 / 0;
say "START";
say $result;
say "END";

CATCH {
    # Let's intropect both the `$!` and `$_` variables.
    say '$_.^name => ', $_.^name;
    say '$!.^name => ', $!.^name;
    say '$_ => ', $_;
    say '$! => ', $!;
}
$ perl6 catch_block.p6
START
$_.^name => X::Numeric::DivideByZero
$!.^name => Nil
$_ => Attempt to divide 1 by zero using div
  in block <unit> at catch_block.p6 line 3

$! => Nil
Attempt to divide 1 by zero using div
  in block <unit> at catch_block.p6 line 3

In this code snippet, the object in $_ is an instance of the X::Numeric::DivideByZero class with the Attempt to divide 1 by zero using div as its message. The X:: namespace is used by convention for the exception classes. In this case, the variable $! is empty (Nil) since this variable is only populated by a try block.

Throwing exceptions

Exceptions in Raku are usually thrown by using the die subroutine. This sub takes the description of an error and throws a fatal exception which terminates the program immediately. The exception can then be handled by either the try or CATCH block.

my $value = 45;
die "$value is over 30" if $value > 30;

CATCH {  
    say "Exception name: ", $_.^name; # obtaining the exception's name by calling
                                      # the `.^name` method on the `Exception`
                                      # object.
}
$ perl6 exception_throwing.p6
Exception name: X::AdHoc
45 is over 30
  in block <unit> at RAKU-QUICK-PROTO.p6 line 2
 

An exception object can be instantiated directly from a specific exception class using the new method. The same exception can be thrown by invoking the method throw on the object. In the last code snippet, the exception type was X::AdHoc which are exceptions that can be used by calling the die sub. Thus the same exception thrown with die sub could be performed by instantiating the X::AdHoc as follows:

my $value = 45;
my $my-exc = X::AdHoc.new;     # instantiating the `X::AdHoc` class

$my-exc.throw if $value > 30;  # throwing an exception
# Or (at once): X::AdHoc.new.throw if $value > 30;

CATCH {
    say "Exception caught!";
}
$ perl6 exception_throwing.p6
Exception caught!
Unexplained error
  in block <unit> at RAKU-QUICK-PROTO.p6 line 4

The program printed the default string Unexplained error. To print a custom error message, pass the named argument payload with the error message to new when creating the exception object.

my $value = 45;

if $value > 30 {
    X::AdHoc.new(payload => "Value mustn't be greater than 30").throw
}

CATCH {
    say "Exception caught!";
}
$ perl6 
Exception caught!
Value mustn't be greater than 30
  in block <unit> at RAKU-QUICK-PROTO.p6 line 4

Resuming from exceptions

Unlike the try block, the CATCH block stops the program execution immediately. In order to return control to where the error happens, invoke the resume method on the topic variable $_ which contains the exception object.

say "START";
X::AdHoc.new(payload => "An error occurred").throw;
say "Still running like nothing happened!";
say "END";

CATCH {
    say "Exception caught!";
    $_.resume;  # returns control
}
$ perl6 resuming_exceptions.p6
START
Exception caught!
Still running like nothing happened!
END

Catching different exceptions

The X:: namespace defines a list of predefined classes that take care of different exceptions. By default, exceptions are stored in the topic variable $_ and the when clause provide a clean way of smartmatching against different types of exceptions inside a CATCH block.

my Int $x = 'String'; # invalid assignment
say 1 / 0;            # division by zero
"no-existent".IO.f;   # testing a non-existent file

CATCH {
    when X::Numeric::DivideByZero {
        say "Division by zero exception";
        $_.resume;
    }
    when X::TypeCheck::Assignment {
        say "Invalid assignment to an Int";
        .resume; # same as `$_.resume`
    }
    default {    # catch any other exception
        say "Any other exception";
    }
}
$ perl6 diff_exceptions.p6
Invalid assignment to an Int
Division by zero exception
Any other exception

For a complete list of the different exception types, please visit https://docs.perl6.org/type-exceptions.html.

Creating custom exceptions

The creation of a custom exception in Raku boils down to subclassing the Exception class which then integrates it into the Raku system. Although the new exception can be created in any namespace, it's conventional to create it in the same namespace as the built-in exceptions; that is the X:: namespace.

class X::ShortPassword is Exception {
    method message {
        "Password is too short"; 
    }
}

sub validate-password( Str $password, Int $length ) {
    if $password.chars < $length {
        X::ShortPassword.new.throw;
    }
    return True;
}

validate-password('KRN7XJFMH63Y', 15);
$ perl6 custom_exceptions.p6
Password is too short
  in sub validate-password at custom_exceptions.p6 line 8
  in block <unit> at custom_exceptions.p6 line 12

For additional info, go to

Back to main