NAME
App::Actions::Error - an exception that carries a stable code
SYNOPSIS
Tell one failure from another by its code:
use App::Actions::Error;
use App::Actions::Workflow;
my $workflow = eval { App::Actions::Workflow->load($file) };
if (!$workflow) {
my $error = App::Actions::Error->caught($@);
if ($error && $error->is('workflow.file_missing')) {
print 'No workflow at ', $error->args->{file}, "\n";
} else {
die $@; # not one we handle: pass it on
}
}
Handle a whole family of failures:
if (my $error = App::Actions::Error->caught($@)) {
exit 3 if $error->is('runner.image.'); # any image problem
}
Show any failure to a person, without the file and line number:
eval { $runner->run; 1 }
or print STDERR 'action-run: ', App::Actions::Error->describe($@), "\n";
Code that treats exceptions as strings keeps working:
eval { App::Actions::Workflow->load('missing.yml') };
print "not found\n" if $@ =~ /does not exist/;
DESCRIPTION
When something goes wrong, this distribution throws (dies with) an App::Actions::Error object. The object has:
a code, such as
workflow.file_missing. The code never changes when the message is translated or reworded, so your program can check it safely;a message, such as
Workflow file 'ci.yml' does not exist, in the user's language;the arguments used to build the message, such as
{ file => 'ci.yml' };a location, such as
at lib/App/Actions.pm line 42., the same textcroakadds.
As a string, the object is exactly the message followed by the location: the same text croak would have died with. So older code that reads $@ as a string, prints it or matches it with a regular expression sees no difference.
You do not usually make these objects yourself. Code inside the distribution calls $self->throw($code, \%args) from App::Actions::I18N, which looks the code up in the message catalogue, translates and fills in the message, and throws the object. The codes are the keys of the catalogue in App::Actions::I18N::en.
METHODS
new
Creates an exception without throwing it. Most code never calls this: it uses $self->throw($key, \%args) from App::Actions::I18N, which renders the message and records where it was thrown from.
ARGUMENTS
code- the catalogue key, such asworkflow.no_jobs.message- the rendered text, without a location.args- optional; the values the message was rendered with.location- optional; what follows the message when it is stringified, normally" at FILE line N.\n". Defaults to"\n".
RETURNS
An App::Actions::Error object.
SIDE EFFECTS
None.
EXAMPLE
my $error = App::Actions::Error->new(
code => 'workflow.empty',
message => "Workflow 'ci.yml' is empty",
args => { file => 'ci.yml' },
);
die $error;
API SPECIFICATION
INPUT
{
code => { type => 'string', matches => qr/\A[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)+\z/ },
message => { type => 'string' },
args => { type => 'hashref', optional => 1 },
location => { type => 'string', optional => 1 },
}
OUTPUT
{ type => 'object', isa => 'App::Actions::Error' }
MESSAGES
+---------------------------+------------------------------+-----------------------------+
| Message | Meaning | Resolution |
+---------------------------+------------------------------+-----------------------------+
| Invalid arguments to | A required argument is | Pass a dotted code and a |
| App::Actions::Error::new | missing or has the wrong type| message string |
+---------------------------+------------------------------+-----------------------------+
code
Returns the catalogue key the exception was raised with.
EXAMPLE
print $error->code; # workflow.file_missing
API SPECIFICATION
INPUT
{}
OUTPUT
{ type => 'string' }
MESSAGES
None.
message
Returns the rendered message without the location, as a person would want to read it.
EXAMPLE
print STDERR 'action-run: ', $error->message, "\n";
API SPECIFICATION
INPUT
{}
OUTPUT
{ type => 'string' }
MESSAGES
None.
args
Returns a copy of the values the message was rendered with, so a caller can act on the file, job or image the failure is about.
EXAMPLE
my $file = $error->args->{file};
API SPECIFICATION
INPUT
{}
OUTPUT
{ type => 'hashref' }
MESSAGES
None.
location
Returns what follows the message when it is stringified: " at FILE line N.\n", a stack trace for an exception raised with trace => 1, or "\n" for one that names no place in the code.
EXAMPLE
warn 'raised', $error->location;
API SPECIFICATION
INPUT
{}
OUTPUT
{ type => 'string' }
MESSAGES
None.
text
Returns the message followed by the location: what the object stringifies to, and what croak would have died with.
EXAMPLE
is("$error", $error->text, 'stringification is the text');
API SPECIFICATION
INPUT
{}
OUTPUT
{ type => 'string' }
MESSAGES
None.
is
Says whether the exception has a code, or belongs to a family of codes.
ARGUMENTS
$code- a full code (workflow.no_jobs), or a prefix ending in a dot (workflow.) for every code that starts with it.
RETURNS
1 or 0.
EXAMPLE
if ($error->is('runner.image.')) { ... } # any image failure
API SPECIFICATION
INPUT
{ code => { type => 'string', min => 1 } }
OUTPUT
{ type => 'boolean' }
MESSAGES
None.
caught
my $error = App::Actions::Error->caught($@);
Returns the value if it is an App::Actions::Error (or a subclass of the class it is called on), and undef otherwise, so a caller can ask for the code without first checking what kind of value $@ holds.
EXAMPLE
if (my $error = App::Actions::Error->caught($@)) {
exit 70 if $error->is('schema.invalid');
}
API SPECIFICATION
INPUT
{ error => { type => 'any', optional => 1 } }
OUTPUT
{ type => 'object', isa => 'App::Actions::Error', optional => 1 }
MESSAGES
None.
describe
print STDERR 'action-run: ', App::Actions::Error->describe($@), "\n";
Returns any exception as a sentence for a person: the message of an App::Actions::Error, or a string exception with its final " at FILE line N." and trailing white space removed. A position the message gives of its own, such as JSON::PP's at character offset 9, is kept, because it is part of the diagnosis.
ARGUMENTS
$error- an exception: an object or a string.undefgives the empty string.
RETURNS
A string.
EXAMPLE
App::Actions::Error->describe("No job matched 'x' at lib/App/Actions.pm line 9.\n");
# No job matched 'x'
API SPECIFICATION
INPUT
{ error => { type => 'any', optional => 1 } }
OUTPUT
{ type => 'string' }
MESSAGES
None.
LIMITATIONS
Only messages raised through the catalogue are objects. Errors from Perl itself, from CPAN modules and from code that still calls
croakwith a literal are strings; "describe" and "caught" accept both so a caller does not have to know which it was given.The code is the catalogue key, so renaming a key is an interface change. The keys are listed in the language modules under App::Actions::I18N.
The arguments are copied when the exception is made, but values in them that are references are shared with the code that threw it.
COMMON PITFALLS
Do not match the message text. It is translated, and its wording may change. Use "is" with the code.
Not every exception is an object. Errors from Perl and from other modules are strings. Use "caught", which returns
undeffor them, before you call a method on$@.isneeds a trailing dot for a family.is('runner.image')matches only that exact code;is('runner.image.')matches every code that starts withrunner.image..argsreturns a copy of the hash, but not of what is inside it. If an argument is a reference, changing what it points to changes the error too.A method call does not work inside a string.
"$error->code"prints the whole error followed by->code. Write'...' . $error->code.The object is always true. Even an error with an empty message counts as true, so
if ($@)works as expected.Keep a copy of
$@. Anyevalyou run later (even inside a destructor) can reset it. Writemy $error = $@;first.
ENCODING
The message is a Perl character string. It may contain any Unicode text, including non-ASCII characters and emoji, because it may be a translation (for example into Chinese) and may quote a workflow's own text. Set an encoding on the handle before you print it, for example binmode STDERR, ':encoding(UTF-8)'. The code is always ASCII: lower-case letters, digits, _ and dots. The location is the file name as Perl knows it.
AUTHOR
Nigel Horne, njh@nigelhorne.com
FORMAL SPECIFICATION
This section describes each routine in the Z notation, a precise mathematical language for describing programs. You do not need to read it to use the module: everything here is also said in plain words in the sections above. It is for readers who want to check exactly what a routine promises.
How to read it: a box names a routine. The lines above the middle rule declare the values it works with. A name ending in ? is an input, and a name ending in ! is an output. ΞState means the routine changes nothing, and ΔState means it can change the state. The lines below the rule are the conditions that are always true when the routine returns.
new
┌─ NewError ────────────────────────────────────────
│ code? : KEY
│ message? : seq CHAR
│ args? : optional (NAME ⇸ VALUE)
│ location? : optional seq CHAR
│ e! : Error
├───────────────────────────────────────────────────
│ e!.code = code? ∧ e!.message = message?
│ e!.args = (if args? = ⊥ then ∅ else args?)
│ e!.location = (if location? = ⊥ then "\n" else location?)
│ text(e!) = e!.message ⁀ e!.location
└───────────────────────────────────────────────────
code
┌─ Code ────────────────────────────────────────────
│ ΞError
│ code! : KEY
├───────────────────────────────────────────────────
│ code! = θError.code
└───────────────────────────────────────────────────
message
┌─ Message ─────────────────────────────────────────
│ ΞError
│ message! : seq CHAR
├───────────────────────────────────────────────────
│ message! = θError.message
└───────────────────────────────────────────────────
args
┌─ Args ────────────────────────────────────────────
│ ΞError
│ args! : NAME ⇸ VALUE
├───────────────────────────────────────────────────
│ args! = θError.args
└───────────────────────────────────────────────────
location
┌─ Location ────────────────────────────────────────
│ ΞError
│ location! : seq CHAR
├───────────────────────────────────────────────────
│ location! = θError.location
└───────────────────────────────────────────────────
text
┌─ Text ────────────────────────────────────────────
│ ΞError
│ text! : seq CHAR
├───────────────────────────────────────────────────
│ text! = θError.message ⁀ θError.location
└───────────────────────────────────────────────────
is
┌─ Is ──────────────────────────────────────────────
│ ΞError
│ code? : seq CHAR
│ r! : 𝔹
├───────────────────────────────────────────────────
│ last(code?) = '.' ⇒ (r! ⇔ code? prefix θError.code)
│ last(code?) ≠ '.' ⇒ (r! ⇔ code? = θError.code)
└───────────────────────────────────────────────────
caught
┌─ Caught ──────────────────────────────────────────
│ class? : CLASS
│ value? : VALUE
│ e! : optional Error
├───────────────────────────────────────────────────
│ value? ∈ class? ⇒ e! = value?
│ value? ¬in; class? ⇒ e! = ⊥
└───────────────────────────────────────────────────
describe
┌─ Describe ────────────────────────────────────────
│ error? : VALUE
│ s! : seq CHAR
├───────────────────────────────────────────────────
│ error? ∈ Error ⇒ s! = rtrim(error?.message)
│ error? = ⊥ ⇒ s! = ⟨⟩
│ otherwise s! = rtrim(stripLocation(rtrim(error?)))
└───────────────────────────────────────────────────
STATE DIAGRAM
An error object never changes after it is made. This is its life:
$self->throw($code, \%args) App::Actions::Error->new(...)
(App::Actions::I18N: look up $code, (rare: a caller that builds
translate, fill in %args, add the its own)
caller's " at FILE line N.") |
| |
+------------------+-------------------+
v
+-----------------+
| CREATED | code, message, args,
| (immutable) | location fixed
+-----------------+
|
| die
v
+-----------------+
| THROWN | in $@
+-----------------+
| | |
not caught by any | | | caught as a string ("$@", =~, print):
eval | | | reads as message . location
v | v
program stops; | old code works unchanged
STDERR shows the |
message and location | App::Actions::Error->caught($@)
v
+-----------------+
| INSPECTED | is($code) / is('family.')
+-----------------+ code / message / args / location
|
| describe($@)
v
message without the location, for a person
LICENSE AND COPYRIGHT
Copyright 2026 Nigel Horne.
This program is released under the GNU General Public License, version 2. See the LICENSE file that comes with this distribution. If you use it, please let me know.