# Exceptions, `die`, and typed error objects This chapter covers how to raise, catch, and classify runtime failures: `die` and [`eval`](../../p5/core/perlfunc/eval), the native `try`/`catch`, special-variable hygiene (`$@`, `$!`, `$?`), `$SIG{__DIE__}`, and the design of exception classes. Readers who know `die` and `eval { }` will find here the modern replacements for string-dispatching on exception messages and the hazards of `$SIG{__DIE__}` that the stock `eval` pattern silently hides. ## `die` and [`eval`](../../p5/core/perlfunc/eval) [`die`](../../p5/core/perlfunc/die) throws an exception; the nearest enclosing `eval { }` catches it and leaves the message in `$@`: ```perl eval { risky(); }; if (my $err = $@) { warn "caught: $err"; } ``` Without a trailing `\n`, `die`'s argument gets `at FILE line N.` appended from the caller's frame. With `\n`, no location is appended — use for messages that will be shown to users, omit for messages you want to trace. Never use the string form `eval "..."` for exception handling. It compiles its argument and has every hazard of user-supplied code. ## The native `try`/`catch` On Perl 5.34 and later, a dedicated `try`/`catch` is available behind a feature flag. Since 5.40 it is non-experimental: ```perl use v5.40; use feature 'try'; try { risky(); } catch ($e) { warn "caught: $e"; } ``` `finally` remains experimental as of 5.42. Expect: ```perl use feature 'try'; no warnings 'experimental::try'; ``` on perls between 5.34 and 5.40. For code that must run on 5.34 or 5.36 and also on newer perls, `Feature::Compat::Try` picks the core feature where available and falls back to `Syntax::Keyword::Try`: ```perl use Feature::Compat::Try; try { risky(); } catch ($e) { warn "caught: $e" } ``` ### Why native `try` over `Try::Tiny` `Try::Tiny` still works and remains common. Prefer the native form for new code: it avoids the sub-call frame that `Try::Tiny` introduces (which shows up in stack traces), has zero startup cost, and composes with the `isa` operator cleanly. These are retired — do not adopt: - `TryCatch` — abandoned. - `Error.pm` — superseded. - `Exception::Class::TryCatch` — superseded by `Throwable`. ## `$@` clobbering — the classic `eval` hazard `$@` is set by `eval`; it is also observable globally. Any code that runs between `eval`'s return and your check on `$@` — a `DESTROY` method, a tied-variable handler, an unrelated nested `eval` — can overwrite it. Always copy first: ```perl eval { risky(); }; my $err = $@; return unless $err; ``` Native `try`/`catch` and `Syntax::Keyword::Try` avoid this pitfall by giving you the error as the `catch` parameter; raw `eval` does not. Prefer the former for any code that takes exceptions seriously. ## Special variables adjacent to exceptions | Variable | Set by | Read immediately because… | |----------|----------------------------|---------------------------| | `$@` | `eval { }` / `die` | any intervening call can overwrite. | | `$!` | Last failed system call | any following syscall overwrites. | | `$?` | `system` / backticks child | the next child overwrites. | | `$^E` | Extended OS error | on non-Unix; on Linux usually mirrors `$!`. | `$?` decoding after `system` / backticks: ```perl 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; ... } ``` ## Classifying exceptions: dispatch on type For non-trivial programs, distinguishing "user not found" from "database is down" from "programming error" by parsing error strings is fragile. Throw typed objects and match on them: ```perl 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"); } } ``` The `isa` operator (stable on 5.36+) does the right thing for overloaded objects and is zero-cost when the left operand is not a reference. Pair it with a typed wrapper at the outermost boundary so everything downstream is a `MyApp::Err`. ## Designing exception classes The 2026 default: `Throwable` role from CPAN, optionally via `Throwable::SugarFactory` for declarative hierarchies. Minimum-viable class: ```perl 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; ``` Usage: ```perl MyApp::Err::NotFound->throw(resource => 'user', id => $uid); ``` `throw` is provided by the role; it dies with `$self` (the blessed object). `->new` without `throw` builds without raising. Declarative form via `Throwable::SugarFactory`: ```perl 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; ``` This generates: the class, a `not_found(id => ...)` constructor- and-throw helper, a `is_not_found($e)` predicate, and `->to_hash` on every instance for JSON serialisation. ### What a class must have 1. **A class name** — so `$e isa MyApp::Err::X` dispatches. 2. **Structured fields** — `id`, `resource`, `code`. 3. **A human `message`** — without it, `warn $e` prints `MyApp::Err=HASH(0x...)`. 4. **Serialisation** — `->to_hash` for logging / aggregators / JSON APIs. 5. **Origin** — file + line captured at `throw`. `Throwable` captures these automatically. 6. **A stack trace** if the error is worth investigating later: `with 'StackTrace::Auto'` adds `->stack_trace`. ### When not to use a class Plain-string `die` remains correct for short scripts, one-offs, and any case where no caller will dispatch on the error: ```perl die "permission denied: $path\n"; ``` The trailing `\n` suppresses file-and-line appending; the message is user-facing. Hash-ref `die` — `die { type => 'not_found', id => $uid }` — is a transitional form. The moment a caller writes `if (ref $e eq 'HASH' && $e->{type} eq 'not_found')` three times, refactor to a class. Retired exception frameworks — do not adopt: - `Error.pm` — use `Throwable` (role-based) or `Exception::Class`. - `Class::Throwable` — functional; `Throwable` is the modern choice. - `Exception::Class::TryCatch` — superseded by native `try`. ## `$SIG{__DIE__}` — handle with care `$SIG{__DIE__}` fires on every `die`, **including** `die` inside `eval`. A naive install turns every caught exception into noise: ```perl # WRONG: fires on every eval that catches an error $SIG{__DIE__} = sub { Carp::confess(@_) }; ``` The `$^S` special variable tells you whether you are inside an `eval`: true means "yes, someone is going to catch this"; false means "this is reaching the top". Guard on it: ```perl $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 }; ``` For Sentry / aggregator integration, wrap the reporter call in its own `eval` — a failing error reporter must not obscure the original exception. Before installing on top of an existing handler, chain: ```perl 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__}` — turn warnings fatal, or route them Symmetric to `$SIG{__DIE__}`, fires on every `warn`: ```perl $SIG{__WARN__} = sub { die $_[0] }; # promote warnings to exceptions ``` Useful during development as a tripwire; rarely wanted in production. More common production use: route warnings through a logger. ## Find out more - [print-and-die](print-and-die) — the Carp family and stack-trace-everywhere switches. - [`perlfunc/die`](../../p5/core/perlfunc/die), [`perlfunc/eval`](../../p5/core/perlfunc/eval), [`perlfunc/warn`](../../p5/core/perlfunc/warn), [`perlvar`](../../p5/core/perlvar) — reference pages for the language primitives.