חריגות, die, ועצמי שגיאה מטופסים#

פרק זה מכסה כיצד להעלות, לתפוס ולסווג כשלים בזמן ריצה: die ו־eval, ה־try/catch המקומי, היגיינה של משתנים מיוחדים ($@, $!, $?), $SIG{__DIE__}, ועיצוב מחלקות חריגה.

קוראים המכירים את die ואת eval { } ימצאו כאן את החלופות המודרניות ל־dispatching מבוסס־מחרוזת על הודעות חריגה ואת הסכנות של $SIG{__DIE__} שתבנית ה־eval הסטנדרטית מסתירה בשתיקה.

die ו־eval#

die זורקת חריגה; ה־eval { } העוטף הקרוב ביותר תופס אותה ומשאיר את ההודעה ב־$@:

eval {
    risky();
};
if (my $err = $@) {
    warn "caught: $err";
}

ללא \n גורר, לארגומנט של die מצורף at FILE line N. מהמסגרת של הקורא. עם \n, לא מצורף מיקום - השתמשו עבור הודעות שיוצגו למשתמשים, השמיטו עבור הודעות שתרצו לעקוב אחריהן.

לעולם אל תשתמשו בצורת המחרוזת eval "..." לטיפול בחריגות. היא מהדרת את הארגומנט שלה ויש לה כל סכנה של קוד שסופק על־ידי משתמש.

ה־try/catch המקומי#

ב־Perl 5.34 ואילך, try/catch ייעודי זמין מאחורי דגל תכונה. החל מ־5.40 הוא אינו ניסיוני:

use v5.40;
use feature 'try';

try {
    risky();
}
catch ($e) {
    warn "caught: $e";
}

finally נותר ניסיוני נכון ל־5.42. צפו ל:

use feature 'try';
no warnings 'experimental::try';

ב־perls בין 5.34 ל־5.40.

לקוד שחייב לרוץ על 5.34 או 5.36 וגם על perls חדשים יותר, Feature::Compat::Try בוחר את התכונה מ־core היכן שזמינה ונופל בחזרה ל־Syntax::Keyword::Try:

use Feature::Compat::Try;

try { risky(); }
catch ($e) { warn "caught: $e" }

מדוע try מקומי על־פני Try::Tiny#

Try::Tiny עדיין עובד ונשאר נפוץ. העדיפו את הצורה המקומית עבור קוד חדש: היא נמנעת ממסגרת קריאת ה־sub ש־Try::Tiny מציג (אשר מופיעה ב־stack traces), יש לה עלות אתחול אפס, והיא מורכבת היטב עם אופרטור isa.

אלה פרושים - אל תאמצו:

  • TryCatch - נטוש.

  • Error.pm - הוחלף.

  • Exception::Class::TryCatch - הוחלף על־ידי Throwable.

דריסה של $@ - הסכנה הקלאסית של eval#

$@ נקבע על־ידי eval; הוא גם נצפה גלובלית. כל קוד שרץ בין החזרת eval לבדיקה שלכם על $@ - מתודת DESTROY, מטפל של משתנה קשור, eval מקונן לא־קשור - יכול לדרוס אותו. תמיד העתיקו תחילה:

eval { risky(); };
my $err = $@;
return unless $err;

try/catch מקומי ו־Syntax::Keyword::Try נמנעים ממלכודת זו על־ידי מתן השגיאה כפרמטר ל־catch; eval גולמי לא. העדיפו את הקודמים בכל קוד שמתייחס לחריגות ברצינות.

משתנים מיוחדים סמוכים לחריגות#

משתנה

נקבע על־ידי

קראו מיד מכיוון ש…

$@

eval { } / die

כל קריאה ביניים יכולה לדרוס.

$!

קריאת המערכת הכושלת האחרונה

כל syscall עוקב דורס.

$?

ילד של system / backticks

הילד הבא דורס.

$^E

שגיאת OS מורחבת

ב־non-Unix; ב־Linux בדרך כלל משקף את $!.

פענוח $? לאחר system / backticks:

if ($? == -1)         { warn "child failed to start: $!" }
elsif ($? & 127)      { warn "child killed by signal " . ($? & 127) }
elsif ($? & 128)      { warn "core dumped" }        # combined with above
else                  { my $exit = $? >> 8; ... }

סיווג חריגות: dispatching לפי טיפוס#

לתוכניות לא־טריוויאליות, הבחנה בין ״משתמש לא נמצא״ ל־״מסד הנתונים מושבת״ ל־״שגיאת תכנות״ על־ידי ניתוח מחרוזות שגיאה היא שבירה. זרקו עצמים מטופסים ובצעו עליהם התאמה:

use v5.40;
use feature 'try';
use Scalar::Util qw(blessed);

try {
    do_business();
}
catch ($e) {
    if    ($e isa MyApp::Err::NotFound)    { return http_404($e) }
    elsif ($e isa MyApp::Err::AuthFailure) { return http_403($e) }
    elsif ($e isa MyApp::Err)              { return http_500($e) }
    elsif (blessed($e) && $e->isa('DBIx::Class::Exception')) {
        return http_500($e);
    }
    else {
        MyApp::Err::Unknown->throw(cause => "$e");
    }
}

אופרטור isa (יציב ב־5.36+) עושה את הדבר הנכון עבור עצמים עם overloading והוא בעלות אפס כאשר האופרנד השמאלי אינו הפניה. שלבו אותו עם עוטף מטופס בגבול החיצוני ביותר כך שכל מה שבזרם מתחת הוא MyApp::Err.

עיצוב מחלקות חריגה#

ברירת המחדל ב־2026: תפקיד Throwable מ־CPAN, אופציונלית דרך Throwable::SugarFactory להיררכיות הצהרתיות.

מחלקה מינימלית בת־קיימא:

package MyApp::Err::NotFound;
use Moo;
with 'Throwable';

has resource => ( is => 'ro', required => 1 );
has id       => ( is => 'ro', required => 1 );

sub message {
    my $self = shift;
    sprintf 'not found: %s/%s', $self->resource, $self->id;
}

1;

שימוש:

MyApp::Err::NotFound->throw(resource => 'user', id => $uid);

throw מסופק על־ידי התפקיד; הוא מבצע die עם $self (העצם ה־blessed). ->new ללא throw בונה מבלי להעלות חריגה.

צורה הצהרתית באמצעות Throwable::SugarFactory:

package MyApp::Err;
use Throwable::SugarFactory;

exception 'GenericError'      => 'something bad happened';
exception 'NotFound'           => 'resource not found'
                                  => (has => [ id => (is => 'ro') ]);
exception 'AuthFailure'        => 'auth failed'
                                  => (has => [ reason => (is => 'ro') ]);
exception 'AuthFailureExpired' => 'session expired'
                                  => (extends => 'AuthFailure');
1;

זה מייצר: את המחלקה, helper של בנאי־וזריקה not_found(id => ...), פרדיקט is_not_found($e), ו־->to_hash בכל מופע לסריאליזציית JSON.

מה מחלקה חייבת להכיל#

  1. שם מחלקה - כך ש־$e isa MyApp::Err::X מבצע dispatching.

  2. שדות מובנים - id, resource, code.

  3. message אנושי - בלעדיו, warn $e מדפיס MyApp::Err=HASH(0x...).

  4. סריאליזציה - ->to_hash עבור לוגינג / aggregators / JSON APIs.

  5. מקור - קובץ + שורה נלכדים ב־throw. Throwable לוכד אותם אוטומטית.

  6. stack trace אם השגיאה שווה חקירה מאוחר יותר: with 'StackTrace::Auto' מוסיף ->stack_trace.

מתי לא להשתמש במחלקה#

die במחרוזת פשוטה נשאר נכון לסקריפטים קצרים, חד־פעמיים, וכל מקרה שבו אף קורא לא יבצע dispatching על השגיאה:

die "permission denied: $path\n";

ה־\n הגורר מדכא הצמדת קובץ־ושורה; ההודעה פונה למשתמש.

die עם hash-ref - die { type => 'not_found', id => $uid } - היא צורת מעבר. ברגע שקורא כותב if (ref $e eq 'HASH' && $e->{type} eq 'not_found') שלוש פעמים, בצעו refactor למחלקה.

מסגרות חריגה פרושות - אל תאמצו:

  • Error.pm - השתמשו ב־Throwable (מבוסס תפקיד) או ב־Exception::Class.

  • Class::Throwable - פונקציונלי; Throwable היא הבחירה המודרנית.

  • Exception::Class::TryCatch - הוחלף על־ידי try מקומי.

$SIG{__DIE__} - לטפל בזהירות#

$SIG{__DIE__} מופעל בכל die, כולל die בתוך eval. התקנה נאיבית הופכת כל חריגה נתפסת לרעש:

# WRONG: fires on every eval that catches an error
$SIG{__DIE__} = sub { Carp::confess(@_) };

המשתנה המיוחד $^S אומר לכם האם אתם בתוך eval: אמת משמעה ״כן, מישהו יתפוס את זה״; שקר משמעו ״זה מגיע לראש״. שמרו עליו:

$SIG{__DIE__} = sub {
    return if $^S;               # inside eval - not for us
    my ($err) = @_;
    eval { ship_to_sentry($err) };    # never let the handler itself die
    warn "reporter failed: $@" if $@;
    die $err;                    # re-raise
};

עבור שילוב עם Sentry / aggregator, עטפו את קריאת המדווח ב־eval משלה - מדווח שגיאות כושל אסור שיסתיר את החריגה המקורית.

לפני התקנה מעל מטפל קיים, שרשרו:

my $previous = $SIG{__DIE__};
$SIG{__DIE__} = sub {
    return if $^S;
    my ($err) = @_;
    eval { ship_to_sentry($err) };
    warn "reporter failed: $@" if $@;
    goto &$previous if $previous;
    die $err;
};

$SIG{__WARN__} - להפוך אזהרות ל־fatal, או לנתב אותן#

סימטרי ל־$SIG{__DIE__}, מופעל בכל warn:

$SIG{__WARN__} = sub { die $_[0] };      # promote warnings to exceptions

שימושי בזמן פיתוח כ־tripwire; כמעט אף פעם לא רצוי בייצור. שימוש נפוץ יותר בייצור: לנתב אזהרות דרך logger.

למידע נוסף#