Catch Control

Published: (February 3, 2026 at 02:25 PM EST)
6 min read
Source: Dev.to

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 CATCH thunk: 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 CATCH block, 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 0 errors 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 exceptionDescription
doneCall “done” callback on all taps
emitSend item to all taps of a supply
lastExit the loop structure
nextStart next iteration in a loop
proceedResume after a given block
redoRestart iteration in a loop
returnReturn from a sub/method
succeedExit a given block
takePass item to gather
warnEmit 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

  • CATCH and CONTROL phasers are scope‑based; you cannot introspect a Block at runtime to discover whether it contains a CATCH or CONTROL phaser.
  • 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

  • CATCH handles fatal exceptions.
  • CONTROL handles “normal” control flow exceptions such as next, 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!

Back to Blog

Related posts

Read more »

Two Bugs, One Symptom

Background A debugging war story from implementing an SSE client transport in the Raku MCP SDK. The task seemed straightforward: add legacy SSE transport to th...

Doc Mirages

Introduction This is part seven in the Cases of UPPER series, describing the Raku syntax elements that are completely in UPPERCASE. It discusses the final set...