Mathematica Programming » Code Structure
Error Handling
As we noted before, errors in Mathematica are much more subtle than in many languages. There is no compiler, so nothing will warn you about compile-time errors, and there is no real concept of an automatic runtime error—i.e. there is nothing in the language enforcing any type of strict semantics. Because of this we need to be particularly careful about validating errors ourselves.
When we catch one of these errors we then have the question of what to do with it. To compound the difficulty, there aren't any official guidelines on the best ways to handle errors. I have my own preferences, and so we'll work through the various cases in order of least-to-most suggested.
Abort
When an error is caught, we could simply do as most programming languages do and simply abort the calculation, in this case via the function Abort
.
The problem with this is that it is a potential overreaction as if we are producing code that others will use, this Abort
will be passed up into any of their functions, too.
Message
The standard way Mathematica functions handle cases like this is by emitting a Message
and then returning some type of failure tag, e.g. something like this:
Integrate[x, {1, 10}]
Integrate[x,{1,10}]
Since this was an unhandled-arguments type of error, the function simply bottoms out and says: "I'm going to stay unevaluated".
For those interested, this type of behaviour most commonly occurs when a Condition
is failed in either the pattern test of the function arguments or if a Condition
is found at the end of a scoping construct, e.g.:
doNothing[]:=
Module[
{},
doNothing::no="Nope :)";
"Something!"/;If["Something"=!=Nothing, Message[doNothing::no], True]
]
doNothing[]
doNothing[]
If Abort
is too much, this approach is too little. An error caught deep inside a function shouldn't have to be passed step-by-step up through the rest of the function.
Throw / Catch
The Throw
/ Catch
mechanism provides a much more robust way to handle errors in Mathematica. Catch
takes a second argument that can be a pattern to catch the different error tags that Throw
can attach to its results. By doing this we can get deeply-nested functions to throw to high-level and be caught only by the handlers we want, for instance we can do something like:
main[]:=
NumberForm[Catch[{sub1[], sub2[], sub3[]}, "err1"|"err2"|"err3"], 3]
sub1[]:=
If[#<.25, Throw[StringForm["`` too large", #], "err1"], #]&@RandomReal[]
sub2[]:=
If[#<.5, Throw[StringForm["`` two large", #], "err2"], #]&@RandomReal[]
sub3[]:=
If[#<.75, Throw[StringForm["`` three large", #], "err3"], #]&@RandomReal[]
And when we use this:
ToString/@Table[main[], 10]
{"0.137 two large","0.18 too large","0.704 three large","0.18 too large","0.0282 three large","0.463 three large","0.0129 too large","0.634 three large","0.104 too large","0.464 two large"}
We see that the different errors were caught, each with their messages, and we didn't have to catch and pass up on each round.
Standardized Error Handling
We can combine these approaches to make a more robust approach to error handling. We'll use Message
to tell the user about the source of the error, Throw
and Catch
to pass the error to top-level cleanly, and finally we'll use the Failure
object to capture the message feed them to Throw
.
We can go one step further, though, and allow any exceptions caught by Catch
to optionally be passed up
Here's a sample way this could work:
Protect[$$tagStack];
catchExceptions[expr_, tags:{___}, passUp:True|False:True]:=
Block[
{
$$tagStack=
If[ListQ@$$tagStack, {Join@@$$tagStack, tags}, {{}, tags}]
},
Catch[expr,
Alternatives@@tags,
If[passUp,
If[MemberQ[First@$$tagStack, #2],
Throw[##],
#
]&,
#&
]
]
];
catchExceptions~SetAttributes~HoldFirst;
raiseException[HoldPattern[msg:MessageName[sym_, tag_, ___]], params___]:=
(
Message[msg, params];
Throw[
Failure[tag,
<|"MessageTemplate":>msg, "MessageParameters"->{params}|>],
tag
]
);
raiseException~SetAttributes~HoldFirst;
We create a function that sets up a stack of tags being checked for, calls Catch
with the set of currently tracked tags, if the error is caught, it conditionally checks whether the tag is in the head of the stack and if so passes it up.
Next we take a function that will Throw
to this Catch
block and return a nice structure to go with it.
Here's this in action:
myCoolFunction::err1="Uh oh spaghetti-Os!";
myCoolFunction::err2="Oh deary me!";
myCoolFunction::err3="Why did you do this to me ``?";
myCoolFunction[name_]:=
catchExceptions[
winAwards@
callMeBy@
yourName@name,
{"err1", "err2", "err3"}
];
winAwards[movie_]:=
If[!StringQ@movie,
raiseException[myCoolFunction::err1],
StringForm["`` won an award!", movie]
];
callMeBy[name_]:=
If[RandomReal[]<.5,
raiseException[myCoolFunction::err2],
"Call Me By "<>name//Quiet
];
yourName[name_]:=
If[StringQ@name&&StringFreeQ[name, " "],
raiseException[myCoolFunction::err3, name],
name
];
And now we can use it on some examples:
myCoolFunction/@
Join[
StringRiffle@*DeleteDuplicates/@
Tuples[EntityValue[RandomEntity["GivenName", 2], "Label"], 2],
{1}
]
And we can nest this without end and it will always throw all the way up to top-level:
catchExceptions[
catchExceptions[myCoolFunction[1], {"err1", "err2", "err3"}],
{"err1", "err2", "err3"}
]
More extensions can be built on top of this, but this gets us a good ways beyond passing failed arguments up through our call stack.