Skip to content

Instantly share code, notes, and snippets.

@atweiden
Last active August 27, 2022 07:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save atweiden/e59cf999308425216e157808a823dffd to your computer and use it in GitHub Desktop.
Save atweiden/e59cf999308425216e157808a823dffd to your computer and use it in GitHub Desktop.
Crash course on Raku error handling
use v6;
=head DESCRIPTION
=pod A crash course on Raku error handling.
# ------------------------------------------------------------------------------
('-' xx 78).join.say; ''.say;
# ------------------------------------------------------------------------------
=head2 Basic error handling
#| Class C<X::A> is an C<Exception> with a single required attribute,
#| C<$.from>.
my class X::A
{
also is Exception;
#| C<$.from> is the "source" of the exception.
has Str:D $.from is required;
method message(::?CLASS:D: --> Str:D) { "message from $.from" }
}
# Raku's C<fail> resembles the semantics of the C<Err> variant of Rust's
# C<Result> type insofar as they can be handled with mere if statements.
multi may-fail(Bool:D :fail($)! where .so --> Int:D) { fail X::A.new(:from<fail>) }
multi may-fail(Bool :fail($) --> Int:D) { 0 }
# Raku's C<die> resembles the semantics of Rust's C<panic>, but unlike
# Rust, Raku can catch and handle these "panics". In Raku, catching and
# handling panics entails peppering your code with C<try> and/or C<CATCH>
# blocks.
multi may-die(Bool:D :die($)! where .so --> Int:D) { die X::A.new(:from<die>) }
multi may-die(Bool :die($) --> Int:D) { 0 }
# There's no practical difference between C<may-throw> and C<may-die>,
# other than the return type in C<may-throw>'s signature having to be
# inferred (it's a C<BOOTException>, a low level C type defined by
# MoarVM, for some reason).
#
# "C<throw> can be viewed as the method form of C<die>, just that in
# this particular case, the sub and method forms of the routine have
# different names." (L<Source|https://docs.raku.org/language/exceptions>)
multi may-throw(Bool:D :throw($)! where .so) { X::A.new(:from<throw>).throw }
multi may-throw(Bool :throw($) --> Int:D) { 0 }
# C<Failure>s can be handled with the C<with> control flow statement.
with may-fail() { .say } else { .exception.message.say }
with may-fail(:fail) { .say } else { .exception.message.say }
''.say;
# C<Exception>s can be handled with C<try> blocks.
with try may-die() { .say } else { $!.message.say }
with try may-die(:die) { .say } else { $!.message.say }
''.say;
# C<Exception.throw> and C<die> are equivalent.
with try may-throw() { .say } else { $!.message.say }
with try may-throw(:throw) { .say } else { $!.message.say }
''.say;
{
# An unsightly C<CATCH> block.
CATCH
{
# Match on a specific type of C<Exception>.
when X::A
{
'[X::A] '.print; .message.say;
# Resume control flow from precisely where the C<Exception>
# was thrown, even if it was thrown inside a function.
.resume;
}
default
{
.raku.say;
}
}
may-die().say;
# N.B. C<.say>ing the following line would print "Nil".
may-die(:die);
''.say;
may-throw().say;
may-throw(:throw);
''.say;
die 'ad hoc';
q:to/EOF/.trim.say;
This string isn't printed, because the C<CATCH> block has no special
handling for C<X::AdHoc>, and the C<default> arm of the <CATCH> block
doesn't call C<.resume>.
EOF
}
# ------------------------------------------------------------------------------
''.say; ('-' xx 78).join.say; ''.say;
# ------------------------------------------------------------------------------
=begin pod
=head2 Rust-like error handling
Compared to Rust, this approach lacks enum support, which is unfortunate.
In Rust, error types are commonly implemented as enums, and because Rust
enforces exhaustiveness checks on its enums, you can be very sure every
error variant gets handled.
In Raku, implementing C<Exception> classes as parametric roles doesn't
work for some reason. If it did work, you could create a Rust-y "enum-ish"
error type in Raku by distinguishing parametric role C<Exception>s by enum
variant, e.g.
my enum BannedColour <RED GREEN BLUE>;
my role X::Banned[BannedColour:D $ where BannedColour::RED] { also is Exception; }
my role X::Banned[BannedColour:D $ where BannedColour::GREEN] { also is Exception; }
my role X::Banned[BannedColour:D $ where BannedColour::BLUE] { also is Exception; }
However, even if Raku C<Exception>s, like the ones in the following
example, I<could> be implemented as Rust-y enum-like parametric roles,
Raku doesn't enforce exhaustiveness checks on its enums, unlike Rust.
=end pod
my class X::Banned::Red
{
also is Exception;
method message(::?CLASS:D: --> Str:D) { 'red is banned' }
}
my class X::Banned::Green
{
also is Exception;
method message(::?CLASS:D: --> Str:D) { 'green is banned' }
}
my class X::Banned::Blue
{
also is Exception;
method message(::?CLASS:D: --> Str:D) { 'blue is banned' }
}
sub fmt-hex(Int:D $hex --> Str:D) { '0x' ~ sprintf('%06x', $hex).uc }
#| N.B. C<.resume>ing isn't possible here, because in all cases the
#| C<Exception> is handled - either by a conditional check in the case
#| of C<fail>, or by a C<try> block in the case of C<die>.
#|
#| =item1 "Can only resume an exception object"
#| =item2 Error message printed in C<fail> case
#| =item1 "Too late to resume this exception"
#| =item2 Error message printed in C<die> case
#|
#| multi sub handle-ban(Exception:D $e --> Str:D) { $e.resume }
multi sub handle-ban(X::Banned::Red:D $ --> Str:D) { fmt-hex(0xFF0000) }
multi sub handle-ban(X::Banned::Green:D $ --> Str:D) { fmt-hex(0x00FF00) }
multi sub handle-ban(X::Banned::Blue:D $ --> Str:D) { fmt-hex(0x0000FF) }
multi sub handle-ban(Exception:D $e --> Str:D) { '[WARN] Colour not banned' ~ $e.message }
multi sub may-fail-poly('red' --> Str:D) { fail X::Banned::Red.new }
multi sub may-fail-poly('green' --> Str:D) { fail X::Banned::Green.new }
multi sub may-fail-poly('blue' --> Str:D) { fail X::Banned::Blue.new }
multi sub may-fail-poly('indigo' --> Str:D) { fmt-hex(0x4B0082) }
multi sub may-fail-poly('violet' --> Str:D) { fmt-hex(0xEE82EE) }
multi sub may-fail-poly($ --> Str:D) { fail '?' }
multi sub may-die-poly('red' --> Str:D) { die X::Banned::Red.new }
multi sub may-die-poly('green' --> Str:D) { die X::Banned::Green.new }
multi sub may-die-poly('blue' --> Str:D) { die X::Banned::Blue.new }
multi sub may-die-poly('indigo' --> Str:D) { fmt-hex(0x4B0082) }
multi sub may-die-poly('violet' --> Str:D) { fmt-hex(0xEE82EE) }
multi sub may-die-poly($ --> Str:D) { die '?' }
# Similar to Rust's C<if let Ok(_) = fallibleFunction() {} else {}>. It
# could also stand in for C<if let Some(_) {} else {}>.
with may-fail-poly('red') { .say } else { .exception.&handle-ban.say }
with may-fail-poly('green') { .say } else { .exception.&handle-ban.say }
with may-fail-poly('blue') { .say } else { .exception.&handle-ban.say }
with may-fail-poly('indigo') { .say } else { .exception.&handle-ban.say }
with may-fail-poly('violet') { .say } else { .exception.&handle-ban.say }
with may-fail-poly('xyz') { .say } else { .exception.&handle-ban.say }
''.say;
# Raku's C<Failure> type knows whether it's been handled. Here it was
# handled by the C<with> control flow statement.
with may-fail-poly('red') { .say } else { .handled.say; .raku.say }
''.say;
# Same effect as the above, but more visually distinct. The intent of the
# programmer is also much more obvious at a glance.
with try may-die-poly('red') { .say } else { $!.&handle-ban.say }
with try may-die-poly('green') { .say } else { $!.&handle-ban.say }
with try may-die-poly('blue') { .say } else { $!.&handle-ban.say }
with try may-die-poly('indigo') { .say } else { $!.&handle-ban.say }
with try may-die-poly('violet') { .say } else { $!.&handle-ban.say }
with try may-die-poly('xyz') { .say } else { $!.&handle-ban.say }
# ------------------------------------------------------------------------------
''.say; ('-' xx 78).join.say;
# ------------------------------------------------------------------------------
=begin pod
=head SUMMARY
=item Prefer C<die> over C<fail> to simulate Rust's C<Result> and C<panic>
=item Prefer C<fail> over C<die> to simulate Rust's C<Option>
=item In either case, prefer the Raku version of Rust's C<if let> pattern
In general, use C<die> over C<fail> for error handling in Raku, because it
makes your intent more obvious. Using C<die> along with C<try> and C<$!>
clearly signals to code reviewers you intend to handle a potential error.
C<fail> is best reserved for situations where you're certain you'll always
handle a particular function's return value with either a definedness or
boolean check, e.g.
if may-fail() { do-something() } else { do-something-else() }
Using C<fail> for error handling is suboptimal, because the C<Failure>s
returned by C<fail> can be dismissed with a mere conditional statement.
C<die> is a better fit for error handling than C<fail>.
However, because C<Failure>s I<are> treated like C<Nil>s in, and handled
by, conditionals, C<Failure>s can stand in for Rust's C<Option::None>
quite admirably. Technically, C<Failure>s can also stand in for Rust's
C<Result::Err>, but: don't.
=head2 More about C<Failure>
Instead of writing:
my class A {*}
multi sub a($ where .so --> A:D) { A.new }
multi sub a($ --> A:D) {}
Making use of C<Failure>, you could write:
my class A {*}
multi sub a($ where .so --> A:D) { A.new }
multi sub a($ --> A:D) { fail }
The use of C<fail> in the latter example has the benefit of forcing the
caller of C<a> to handle the optional return value. It isn't quite Rust's
C<Option>, but it's better than nothing.
=head2 C<CATCH> blocks
While C<CATCH> blocks also clearly signal your intent to handle potential
errors, they tend to become rather unsightly. OTOH, C<CATCH> blocks have
the benefit of working with either C<Exception>s or C<Failure>s.
=end pod
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment