DEV Community

Cover image for Catch Control
Elizabeth Mattijsen
Elizabeth Mattijsen

Posted on

Catch Control

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 CATCH thunk: 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";
Enter fullscreen mode Exit fullscreen mode

Running the above code will show:

naughty: urgh
alive still
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

would show:

after
alive still
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

would show:

This exception is not resumable
  in block foo at bar line 42
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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 after given block
  • redo - restart iteration in loop structure
  • return - return from sub / method
  • succeed - exit given block
  • take - pass item to gather
  • 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";
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Since warnings are control exceptions, you can "catch" them in a CONTROL phaser like this:

CONTROL {
    when CX::Warn { .resume }
}
say $_ ~ "foo";
Enter fullscreen mode Exit fullscreen mode

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 the CX::Warn class.

which would just show:

foo
Enter fullscreen mode Exit fullscreen mode

Pretty simple, eh? Well, by popular demand there is actually a shortcut for this case, called the quietly statement prefix:

quietly say $_ ~ "foo";
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or if you would like to turn any warning into a runtime exception:

CONTROL {
    when CX::Warn { .throw }
}
say $_ ~ "foo";
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

And then execute the nice subroutine at will:

say "before";
frobnicate "This";
say "after";
Enter fullscreen mode Exit fullscreen mode

which would show:

before
Caught a frobnication: This
after
Enter fullscreen mode Exit fullscreen mode

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)