Last active
August 27, 2022 07:02
-
-
Save atweiden/e59cf999308425216e157808a823dffd to your computer and use it in GitHub Desktop.
Crash course on Raku error handling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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