Exception Handling
Introduction
This tutorial describes exception handling in the Wolfram Language as a means for structured error handling, typically required in larger projects. This part of the language is typically used in software engineering, rather than, e.g. exploratory or small-scale programming.
Who Should Read This Tutorial
You will benefit the most from using the Exceptions framework, and therefore reading this tutorial, if you are a creator of a package or otherwise a project of a relatively large size and complexity. If all your work is interactive and mostly involves writing a few functions, with each one having just a few lines of code, then using exceptions might be an overkill for you.
That said, small projects tend to grow larger, and at some point reach the size and complexity limit, after which paying more serious attention to structured error handling for your project will become justified and may save you time. So it may make sense to be at least somewhat familiar with these tools.
Despite being aimed at more technically inclined users, this tutorial does not assume prior familiarity with exceptions in any programming language. It was designed to contain sufficient explanations to build an understanding of exceptions in the Wolfram Language from the ground up.
What Exceptions Are
Exceptions implement nonlocal control flow, allowing the programmer to abort the current execution and pass certain information immediately to the higher levels of the computation, bypassing intermediate functions. The most common, although not exclusive, reason for using exceptions is to handle certain errors happening deep inside the current evaluation.
The typical exception infrastructure involves three parts:
- Language primitive to throw an exception. In the Wolfram Language, it is ThrowException.
- Language primitive to catch exceptions (usually selectively, based on their types). In the Wolfram Language, this is CatchExceptions.
- An object representing an exception to be propagated. In the Wolfram Language, this is Exception object.
Exceptions vs. Explicit Error Propagation
There are two main alternatives for the error propagation. One is explicit passing of error value (in the Wolfram Language, e.g. $Failed, Failure object, etc.) from the functions being called to the calling functions, until the execution reaches the code that can handle that error. In the Wolfram Language, one can use lexical forms of Enclose and the Confirm family of functions to significantly simplify this process.
This method works well for simple cases and small enough code bases. The main problem with it is that each intermediate function must explicitly process all the errors that are coming through, even when it is not itself in the position to handle them and must merely propagate them further. This clutters its own code that is concerned with its main task, often very significantly.
The other approach is to use exceptions. They transfer the control flow directly to the function that can handle that specific error. The intermediate functions can now focus specifically on their main tasks and assume that the results they get by calling other functions are always valid. The price to pay for this is that the control flow is now nonlocal.
Different programming languages take a different stance regarding the use of exceptions. Most high-level dynamic languages widely employ exceptions as the main error-handling device. The purpose of this tutorial is to explain how one can effectively use exceptions in the Wolfram Language.
Organization of This Tutorial
This document is intended to be both the tutorial and the reference for the Exceptions framework. It can be read sequentially but does not have to be. Each section is sufficiently self-contained and can be read in separation from other sections. Sequential reading may, however, have advantages for a deeper and more complete understanding of the framework.
To support this modularity, some key concepts are repeated across sections so that each chapter provides the necessary context without requiring you to refer back to previous material. This tutorial prioritizes conceptual depth and completeness over absolute brevity.
Below is a list of sections of this tutorial with a brief summary for each section.
- Exceptions Framework in a Nutshell: a mini-tutorial and practical overview of the framework. Read it first; it might contain all you need to get started.
- Basic Exception-Handling Workflow: the core exception handling building blocks illustrated in isolation with minimal extra context and using the simplest possible examples.
- Working with Exception Objects: details on various aspects of an Exception object. Read it to better understand the core of the framework, various ways to create Exception objects and query their properties, convert them to and from Failure objects and a few other details. Subsections:
- Exception Object: brief description of what it is and why it is an important ingredient of the framework.
- Constructing Exception Objects: different forms of the Exception object constructor.
- Validating Exception Objects: validity checks of Exception objects using the ExceptionQ predicate.
- Exception Object Properties: built-in and user-defined properties of an Exception object, how to extract them, etc.
- Converting Exception to Failure and Vice Versa: the title is self-explanatory. Such conversions are important in their own right, but also the Exceptions framework provides convenient shortcuts and features that can significantly simplify your error handling workflows.
- When Does It Make Sense to Return an Exception Object from a Function?: explains when an Exception object makes sense as a return value.
- Exception Types and Type Hierarchies: detailed discussion of exception types and type inheritance. This chapter is important for understanding exception types, the benefits of type inheritance and how to scope your exceptions properly. It also contains a toy implementation of CatchExceptions/ThrowException, which illustrates how they work under the hood and the role of exception types in this mechanism.
- Throwing and Catching Exceptions: details, subtleties and usage patterns for ThrowException and CatchExceptions. This is the main chapter to understand the nuts and bolts of exception throwing and catching. It also contains discussions of several more advanced usage patterns frequently met in practice and contains a small but complete example package that illustrates the typical use of the Exceptions framework. Subsections:
- How exceptions are propagated: a more technical description of the exception propagation algorithm.
- Internal Mechanics: Relation to Throw and Catch: how ThrowException and CatchExceptions work under the hood.
- Throwing Exceptions with ThrowException: details, idioms and usage patterns of ThrowException.
- Catching Exceptions with CatchExceptions: details, idioms and usage patterns of CatchExceptions. Covers some more advanced topics, such as catching exceptions thrown in exception handlers.
- A Complete Example: a small but complete example package that uses all core features of the Exceptions framework.
- Resource Management: this part of the tutorial provides a detailed discussion of resource management (specifically the WithCleanup function) within the context of exceptions. In the Wolfram Language, this interaction involves several technical subtleties; for instance, the general use of WithCleanup alongside exceptions—whether native Throw events or Exception objects—can be problematic in certain scenarios. The tutorial analyzes these challenges and offers a custom solution. While more technically demanding than other chapters, this section is highly recommended, especially if your code performs significant resource management.
- Exceptions vs. Throw and Catch: this section compares ThrowException/CatchExceptions with Throw and Catch and explains the main differences and when which approach is better.
- Differences between Exceptions and Other Types of Failures: this section describes the differences between exceptions and other kinds of failures in the Wolfram Language, such as $Failed, Failure object, TerminatedEvaluation and aborts ($Aborted).
- Confirm/Enclose Interoperability: this section explains how exceptions interoperate with dynamic (tagged) forms of Enclose and the Confirm family of functions. In particular, it explains how one can use hybrid workflows, using both exceptions and Enclose/Confirm paradigms together.
- Performance and Optimizations: in this section, performance characteristics and limitations of the Exceptions framework are discussed. Also, some optimization tips are given for cases when performance requirements are strict enough that such optimizations may become necessary.
Exceptions Framework in a Nutshell
This section is a mini-tutorial within the larger tutorial, which provides a simple overview and explanation of how to use the framework but skips a number of details and more advanced features. If you only have the time to read one section, this is the one that will get you started.
Exception Propagation Process
The exception propagation process always consists of four stages:
- An Exception object is constructed. The two principal parts it contains are its type (or several types) and its payload, usually containing some information about the error. In the Wolfram Language, the Exception symbol is both the exception constructor and the head of the fully constructed Exception object.
- The constructed Exception object is thrown by an appropriate function/language primitive. In the Wolfram Language, one uses ThrowException to accomplish this.
- The thrown Exception object is being propagated through the call stack. This part is taken care of by the system and is strictly internal. But it still makes sense to keep it in mind, since this is where the nonlocal control flow is manifested.
- The propagated Exception object is either caught at some level in the surrounding code or it reaches the top level uncaught. If it gets caught, this happens inside one of the CatchExceptions calls in the enclosing code. Namely, the nearest enclosing CatchExceptions with the specification that matches one or more of the exception types of that particular Exception object.
In many cases, you will not need to call an Exception constructor explicitly, because ThrowException will do that for you. But both conceptually and technically, the step of constructing an Exception object will always be there.
An Example
A simple example will be used to illustrate various aspects of the framework. The sections that follow will modify it to illustrate various features.
Consider the functions f, g and h, such that each calls the next one: f[g[h[x]]]. If h[x] has a problem, but only f knows how to handle it, it often makes sense to report that problem directly to f. This is where exceptions can help.
In the following code, h[x] computes the factorial of the passed argument but must report an overflow if x > 10. The intermediate function g[x] computes some more complex expression and calls h in the process. And the function f can be considered an interface/user-facing function:
ClearAll[f, g, h]
h[x_] := If[x > 10, ThrowException[OverflowException, x], x!]
g[x_] := (h[x] + h[x + 5]) ^ 2
f[x_] := CatchExceptions[OverflowException] @ g[x]Some important features of this code:
- In the case of an overflow, h[x] throws an exception of type OverflowException and the payload x, using ThrowException.
- ThrowException automatically constructs an Exception object internally. One could also use the equivalent code ThrowException @ Exception[OverflowException, x], with the same effect.
- An operator form of CatchExceptions is used: CatchExceptions[spec][expr]. One could also have used CatchExceptions[g[x], OverflowException] instead, with the same effect.
- The spec of CatchExceptions consists of just the OverflowException exception type. It acts like a filter, instructing CatchExceptions to only catch exceptions of this type.
- Since no explicit exception handler was specified, the default exception handler will be used, converting the caught exception to a specific form of a Failure object.
So, this is the case of a normal execution:
f[2]whereas in this case, overflow occurs, and the result is a Failure:
f[7]Functions
You can go a long way toward building exception-based error handling for your project, using only ThrowException and CatchExceptions. However, the framework provides several other functions, which are useful under various circumstances and make it more complete.
The functions (or symbols) comprising the Exceptions framework can be divided in two groups: those that work with Exception objects, and those that work with exception types.
Exceptions
- constructor for Exception object; optional, ThrowException calls it internally
- also, the head of fully formed Exception objects
- ThrowException: constructs and throws an Exception object of specified type or types.
- CatchExceptions: catches exceptions of specified types and passes their data (in the form of an association) to specified exception handlers.
- ExceptionQ: tests that an expression is a valid Exception object.
Exception Types
- RegisterExceptionType: registers a symbol as an exception type, having zero, one or more parent types.
- ExceptionTypes: lists all or some of the registered exception types.
- ExceptionTypeRegisteredQ: checks if the argument is a symbol registered to represent an exception type.
Exception Object
Exception objects are units of exception propagation. An Exception object is a Wolfram Language expression with head Exception, packaging two essential parts that need to be propagated together: the type (or types) of the error and the information about that error.
It is important that the exception type, which gives the error its identity, propagates together with the error information in a self-contained object. This is one of the important differences between the approach taken by the Exceptions framework and e.g. approaches based on using tagged Throw and Catch: Throw[errorInfo,uniqueTag] and Catch[expr,uniqueTag] or using tagged forms of Confirm and Enclose, where propagating the error's information generally does not contain the error's identity (the unique tag).
Exception objects can be constructed explicitly, using one of the forms of the Exception constructor (which are covered in more detail in the section Constructing Exception Objects):
Exception["SomeExceptionType", 42]Or they can also be constructed implicitly, when one uses ThrowException:
ThrowException["SomeExceptionType", 42]Structurally, a constructed Exception object is very similar to the Failure object. One structural difference is that the former typically contains a list of types rather than a single type/tag:
Exception["SomeExceptionType", 42]//InputFormOne can test whether some expression is a valid Exception object by using the ExceptionQ predicate:
ExceptionQ /@ {Exception["SomeExceptionType", 42], Exception[1, 2, 3]}Explicit construction of Exception objects is more rare than implicit. It can be useful in more advanced scenarios.
Uncaught Exceptions
An exception is called uncaught if it propagates to the top level, not having been caught by any surrounding CatchExceptions.
The following version of the previous example adds type-checking for the function h[x] by throwing an exception of a different type, InvalidArgumentType, in the case of the wrong type of argument x:
h[x_Integer] := If[x > 10, ThrowException[OverflowException, x], x!]
h[x_] := ThrowException[InvalidArgumentType, x]
g[x_] := (h[x] + h[x + 5]) ^ 2
f[x_] := CatchExceptions[OverflowException] @ g[x]However, the developer forgot to handle exceptions of the new exception type in f[x]. The input of the wrong type now leads to an uncaught exception propagated to the top level:
result = f[Pi]The fact that this has indeed been an uncaught exception is manifested in the variable result remaining unassigned after this evaluation.
One way the new exception could have been handled is to add its type to the list of handled types in CatchExceptions:
f[x_] := CatchExceptions[{OverflowException, InvalidArgumentType}] @ g[x]f[Pi]It is recommended to catch all exceptions in your user-facing functions. A typical scenario would be to freely throw exceptions in the private part of your package but make sure that all exported public symbols always return a value, catching all exceptions thrown by your internal functions.
These aspects are discussed in more detail in the subsequent sections.
Exception Types
Exception types are one of the central aspects of the framework. They allow you to group and categorize various errors and selectively define the rules of their propagation.
Single Exception Types
Exception types can be either strings or symbols.
Symbols are generally more recommended for at least two reasons:
- They are naturally scoped by their contexts, which makes the chances of catching someone else's exception minimal.
- They also can be registered and form exception type hierarchies, a powerful feature not available for string tags.
Registered Exception Types and Type Hierarchies
Exception types can form type hierarchies. More specifically, symbolic types can be registered as child types of one or more parent types, using the RegisterExceptionType function. String types cannot be registered and therefore cannot have parent types. They can, however, serve as parent types, just as unregistered symbols also can.
In this example, the two previously used exception types are registered as subtypes of the third one:
RegisterExceptionType[OverflowException, ComputationException]
RegisterExceptionType[InvalidArgumentType, ComputationException]where the parent type ComputationException itself has not been registered, and in general is not required to be (unless it also must be a subtype of some other type, which is not the case in this example).
It is important to mention that RegisterExceptionType will:
It will also not work on Locked symbols.
It is strongly recommended to not tinker with or modify the definitions (DownValues, UpValues, etc.) generated by RegisterExceptionType for the symbol you are registering, unless you really know what you are doing, since doing so might break the Exceptions framework functionality for that symbol/exception type.
You can list all currently registered types using ExceptionTypes:
ExceptionTypes[]The main reason to create exception type hierarchies is to use type inheritance. In particular, exceptions of both types can now be caught by CatchExceptions with the single parent type spec. The previous example can now be modified:
h[x_Integer] := If[x > 10, ThrowException[OverflowException, x], x!]
h[x_] := ThrowException[InvalidArgumentType, x]
g[x_] := (h[x] + h[x + 5]) ^ 2
f[x_] := CatchExceptions[ComputationException] @ g[x]The parent type spec in CatchExceptions allows you to catch both the overflow error:
f[7]and the invalid argument type error:
f[Pi]For simplicity, in the above examples, the default exception handler has been used. Custom exception handlers are discussed in the following sections.
You can check whether a given symbol represents a registered exception type, using the ExceptionTypeRegisteredQ predicate:
ExceptionTypeRegisteredQ /@ {OverflowException, InvalidArgumentType, ComputationException}A typical usage scenario will involve a single root exception type defined for some package and several subtypes for specific errors.
In more complex scenarios, the type hierarchies may become deeper and have intermediate types. For example, if your package has subpackages or sub-modules, each of those may have its own root exception type, but all these root types can still be subtypes of the main package root type. Or a certain specific class of errors may be conveniently described by the main type and a number of subtypes (a good example here would be a set of various database errors, where you may have a generic DatabaseError root type and a number of specific subtypes of it).
Propagating Arbitrary Data
In their simplest form, one uses Exception[type, data] or ThrowException[type, data] to build/throw an exception with some information data. Depending on what data is, two outcomes are possible:
- If data is an association, it is appended to the internal data generated by the Exception constructor to form the full data association for an Exception object.
- If it is anything else, it is stored in the resulting exception's data association with the special key "ExceptionPayload".
The following modification of the previous example uses a custom data representation for different exceptions thrown in this code:
h[x_Integer] := If[x > 10, ThrowException[OverflowException, <|"OverflownValue" -> x|>], x!]
h[x_] := ThrowException[InvalidArgumentType, <|"InvalidTypeValue" -> x|>]
g[x_] := (h[x] + h[x + 5]) ^ 2
f[x_] := CatchExceptions[ComputationException] @ g[x]which is now reflected in the resulting exceptions/Failure objects:
{f[7], f[Pi]}Exception Handlers
In the examples so far, CatchExceptions was used with the default handler, which converts the passed exception data to a certain Failure object. This might work as a sensible default, but in many cases, you will want to use custom handler functions.
Using Custom Exception Handlers
The following modification of the previous example will use a custom function handler to handle the caught exceptions:
h[x_Integer] := If[x > 10, ThrowException[OverflowException, <|"OverflownValue" -> x|>], x!]
h[x_] := ThrowException[InvalidArgumentType, <|"InvalidTypeValue" -> x|>]
g[x_] := (h[x] + h[x + 5]) ^ 2
f[x_] := CatchExceptions[ComputationException -> handler] @ g[x]f[7]The handler is passed an association containing exception data as a single argument.
Using Built-in Dispatch in CatchExceptions
You can use the built-in dispatch mechanism in CatchExceptions to handle different types of exceptions differently.
This code modifies the previous example to handle different exceptions using different handler functions:
f::ovrflw = "Overflow occurred for argument ``";
f::invtp = "The argument `` must be an integer";
f[x_] := CatchExceptions[{
OverflowException -> Function[Message[f::ovrflw, #OverflownValue];$Failed],
InvalidArgumentType -> Function[Message[f::invtp, #InvalidTypeValue];$Failed]
}] @ g[x]And now the user-facing function f[x] issues specific error messages:
f[7]f[Pi]Cleanup
Registered exception types can be deregistered using DeleteObject, but this is not a common operation. Much more commonly, exception types will be registered but never deregistered. You can find more details on this at the end of the section Exception Types and Type Hierarchies.
This clears the symbols used throughout this section:
Unprotect[OverflowException, InvalidArgumentType]
DeleteObject /@ {OverflowException, InvalidArgumentType}
ClearAll[f, g, h]Basic Exception-Handling Workflow
Quick Recap of Basic Building Blocks
The two main operations are throwing exceptions (ThrowException) and catching exceptions (CatchExceptions).
ThrowException["InvalidData", 42]ThrowException @ Exception["InvalidData", 42]CatchExceptions[ThrowException["InvalidData", 42], "InvalidData" -> handler]CatchExceptions[ThrowException["InvalidData", 42], "InvalidData"]Exception construction using an Exception constructor is an optional step. It can be useful in certain more advanced scenarios.
Most Basic Workflow: ThrowException-CatchExceptions
In the most basic form, all one needs to do to propagate an exception is to throw it in one function and catch it in some of the surrounding functions.
Use ThrowException to Throw an Exception
squareInteger[val_] := If[IntegerQ[val], val ^ 2, ThrowException[NonIntegerArgument, val]]squareInteger[3]Exception Propagation
squareInteger[Pi]res = squareInteger[Pi]{squareInteger[Pi], Pi ^ 2}Use CatchExceptions to Catch an Exception
To catch an exception, use CatchExceptions, specifying exception type to catch and an exception handler function.
CatchExceptions[squareInteger[1], NonIntegerArgument -> handler]CatchExceptions[squareInteger[Pi], NonIntegerArgument -> handler]CatchExceptions[squareInteger[Pi], NonIntegerArgument -> Function[
Failure[#ExceptionTag, <|"InvalidValue" -> #ExceptionPayload|>]
]]CatchExceptions[squareInteger[Pi], NonIntegerArgument]Operator Form of CatchExceptions, and Code Ergonomics
Using the operator form of CatchExceptions is often both more convenient and produces more readable code.
CatchExceptions[NonIntegerArgument -> handler][squareInteger[Pi]]The difference in code ergonomics becomes particularly pronounced for larger blocks of code, where one can use prefix function call notation (@) for CatchExceptions[…].
CatchExceptions[NonIntegerArgument -> handler] @ Module[
{a = 1, result},
Print["Before calling squareInteger"];
result = a + squareInteger[Pi];
Print["After calling squareInteger"];
result
]Working with Exception Objects
This section describes in detail how to create and work with Exception objects. You can skip it on the first reading, but the material of this section directly transfers also to the uses of ThrowException.
Somewhat paradoxically, whereas Exception objects are fundamental to the Exceptions framework, their explicit use is not required in most exception-handling workflows, for two reasons:
- ThrowException calls the Exception constructor internally, so that you do not have to.
- Exception handlers in CatchExceptions are passed an association containing the caught exception's data, rather than the full caught Exception object.
So you may use the Exceptions framework for a long time and not ever encounter Exception objects, other than when exceptions are uncaught and propagate to the top level. This design has been intentional, to discourage the routine use of Exception objects as return values of functions in places where one should instead use Failure objects or other values that represent failures.
This means that you do not need to know much about the Exception object to start using the framework effectively. However, there are still good reasons to understand it:
- Since ThrowException uses the same syntax and takes the same arguments as the Exception constructor, learning about the Exception object will automatically give you a detailed understanding of various ways to use ThrowException.
- Exception objects, being the units of exception propagation, are fundamental to the better understanding of the framework.
- For certain more advanced workflows, a good understanding of Exception objects will help.
Exception Object
An exception object is a Wolfram Language expression with head Exception, constructed in a specific way and having a specific structure, which encapsulates all the information about the exception being propagated. It consists of the two basic parts:
- Exception's identity, represented by a list of exception types. This part is used to determine whether or not the exception should be caught by specific clauses in CatchExceptions.
- Exception's data, represented by an association. This part (extended with exception types, which is added to it with the key "ExceptionTagList") is passed to exception handler functions in CatchExceptions.
Fully formed Exception object is very similar in structure to a Failure object.
Exception["SomeError", 42]//InputFormWhat is different is that Exception is not an inert symbol, and in general Exception[args] is a constructor that performs nontrivial computation to return a fully formed Exception object. The other important difference between Exception objects and most other Wolfram Language expressions is that Exception objects rarely appear in code directly, i.e. being returned from some functions and passed to others.
As previously noted, it is not strictly necessary to explicitly construct Exception objects in most cases, but it is still useful to understand Exception objects to better understand the framework and also for some more advanced usage scenarios.
Constructing Exception Objects
There are a number of ways one can construct Exception objects.
Initialization
For the purposes of this section, register the following symbolic exception types:
RegisterExceptionType[abcException]
RegisterExceptionType[abcWrongValueException, abcException]Minimal Way: Using Just the Type
One does not need any exception data or payload to construct the very minimal, bare-bones Exception object.
Exception["SomeError"]Exception[SomeSymbolicException]Exception[abcWrongValueException]//InputFormFrom the Type and Explicit Data (Association)
This is the base, or main, case for the Exception constructor. All the other forms of exception construction (except for the low-level constructors, to be discussed in the following sections) are normalized to this one. In particular, Exception[type] actually is normalized to Exception[type,<||>] before creating an Exception object.
Exception["SomeError", <|"Data" -> 42|>]Exception[SomeSymbolicException, <|"ExpectedInput" -> 42, "ActualInput" -> 24|>]Exception[abcWrongValueException, "Value" -> 100]From the Type and Arbitrary Payload
This method is a shortcut, where the second argument to an Exception constructor can be any expression and it is then added to the exception data with the key "ExceptionPayload".
Exception["SomeError", 42]Exception["SomeError", <|"ExceptionPayload" -> 42|>]Low-Level Constructor Directly from Exception Data
This method is considered lower level, because with it, one must provide certain information that normally is generated by the framework itself. It is useful in more advanced scenarios, such as manual exception rethrowing inside the exception handler function and some others. It also is usually faster. Still, this is a more specialized constructor, and one should only use this when necessary.
The minimal data one has to provide for this constructor is the list of all exception types, similar to how it appears in a fully formed Exception object, with the key "ExceptionTagList".
Exception[<|"ExceptionTagList" -> {"SomeError"}|>]Exception[<|"ExceptionTagList" -> {"SomeError"}, "Data" -> 42|>]With this method, the framework will take the passed data at face value and will not process it in any significant way. This means, in particular, that for exception types that have parents, it is the user's responsibility to list them (unlike other construction methods).
Exception[<|"ExceptionTagList" -> {abcWrongValueException}, "Value" -> 100|>]Exception[<|"ExceptionTagList" -> {abcWrongValueException, abcException}, "Value" -> 100|>]It is more common to use this method when exception data already exists somehow.
exc = Exception[abcWrongValueException, 42]Exception[Append[exc["ExceptionData"], "ExtraData" -> 100]]In particular, one such case can naturally be encountered in the process of exception handling.
CatchExceptions[abcException -> Function[If[#ExceptionPayload > 10,
Print[#];ThrowException @ Exception[Append[#, "ExceptionRethrown" -> True]]]]
]@ ThrowException @ Exception[abcWrongValueException, 42]1. Exception was created by Exception[abcWrongValueException,42].
2. It was then thrown by ThrowException.
3. Thrown exception was caught by CatchExceptions and its data passed to the handler (Function[If[#ExceptionPayload>10,…]]).
4. Since the exception payload (42) is larger than 10 (the condition in the exception handler), the body of If executes. The first thing it does is to print the caught exception's data.
5. Append is used to append some new data to the passed exception data.
6. A new exception is created, using a low-level Exception constructor on the new appended data.
7. New exception is thrown by ThrowException.
CatchExceptions[abcException -> Function[If[#ExceptionPayload > 10,
Print[#];ThrowException [Append[#, "ExceptionRethrown" -> True]]]]
]@ ThrowException @ Exception[abcWrongValueException, 42]Idempotency of Exception Constructor
An Exception constructor is idempotent, i.e. it acts as a no-op when passed a fully formed valid Exception object.
exc = Exception[abcWrongValueException, <|"Value" -> 100|>]Exception[exc] === excCopy Constructor
This is a convenience, which allows one to add some data to an already constructed exception. In this form, the Exception constructor takes a valid Exception object as a first argument and some data association as the second argument. If new data collides with existing data, the new data takes precedence (except for system properties, set by the framework).
exc = Exception[abcWrongValueException, <|"Value" -> 100|>]Exception[exc, <|"Value" -> 200, "ExtraData" -> 42|>]The copy constructor can be useful in the more advanced scenarios, where, for example, one creates various shortcut functions that conveniently produce Exception objects. It is also utilized by the framework internally.
Errors in Exception Construction
Not every input to the Exception constructor is valid. Some such erroneous inputs result in the construction of the special exceptions generated by the framework. In other cases, the expression with an Exception head is just returned unevaluated.
Exception[42]Exception[1, 2, 3]Exception @ Exception[1, 2, 3]There may also be cases when the intended Exception is constructed but with some portion of the passed data being ignored or overwritten by the framework. This happens when one tries to override certain system properties that can only be set by the framework, such as "ExceptionTagList" or "ExceptionTag" (which is only allowed in low-level constructors, where one of these keys is actually required).
Exception["SomeError", <|"Data" -> 42, "ExceptionTagList" -> {"SomeOtherError"}|>]There is a fine balance between an aggressive approach, where any expression with the head Exception that is not understood by the framework is considered a hard error (and thus converted to the system exception of type "ErrorHandlingException"), and the opposite case, where Exception[args] expressions behave mostly as inert (like, e.g. with Failure objects). This part of the design of the current version of the Exceptions framework was an attempt to find some middle ground, but some of the current design choices may change in the future.
Validating Exception Objects
It is possible to check whether or not an expression represents a valid Exception object by using the ExceptionQ predicate. There are at least two reasons to perform such checks:
- The Exception symbol is both a constructor for exceptions and the head for the fully constructed Exception objects. This means that one generally needs to distinguish between the two cases.
ExceptionQ @ Exception[abcWrongValueException, <|"Value" -> 100|>]ExceptionQ @ Unevaluated @ Exception[abcWrongValueException, <|"Value" -> 100|>]ExceptionQ[Exception[1, 2, 3]]It is possible to use the two-argument form of ExceptionQ to test that an expression is a valid Exception object of a specific type. Due to type inheritance, this also works for subtypes of some parent types.
ExceptionQ[Exception[abcWrongValueException, <|"Value" -> 100|>], abcWrongValueException]ExceptionQ[Exception[abcWrongValueException, <|"Value" -> 100|>], abcException]It is important to keep in mind that since Exception objects rather rarely appear in code, it is not expected that ExceptionQ will be very widely used either. The main use case for it is probably to check the validity of Exception objects generated by some convenience or helper functions, which many projects may introduce. Still, ExceptionQ has valid use cases and is required for the framework's consistency.
Exception Object Properties
A valid Exception object has a number of properties. Some of them are automatically set or generated by the framework, and others may represent the user data. It is possible to query and use these properties.
For the purposes of this section, the following exception will be created and stored in a variable:
exc = Exception[abcWrongValueException, <|"Value" -> 200, "ExtraData" -> 42|>]All Properties
To get the names of all the properties, one can use the standard "Properties" property.
exc["Properties"]Some properties are physically present in the exception, whereas others are computed on the fly.
exc//InputFormSystem/Built-in Properties
All valid Exception objects have a number of properties that are either set or computed on the fly by the framework/system itself. Typically, these properties have names that start with "Exception". Here is a partial list of those.
| "ExceptionTagList" | all exception types for this exception | |
| "ExceptionTag" | main exception type for this exception | |
| "ExceptionData" | exception data, computed on the fly | |
| "ExceptionAPIVersion" | the version number of exception internal API | |
| "ExceptionFailureTag" | tag to be used for Failure conversion, optional | |
| "ExceptionFailure" | equivalent Failure object, computed on the fly | |
| "ExceptionPayload" | data payload, optional | |
| "ExceptionValidated" | internal flag set by Exception constructor |
These properties divide into two groups:
- Cannot be overridden by the user: "ExceptionTagList", "ExceptionTag", "ExceptionData", "ExceptionAPIVersion", "ExceptionFailure", "ExceptionValidated".
The following examples demonstrate these properties.
exc["ExceptionTagList"]exc["ExceptionTag"]exc["ExceptionData"]exc["ExceptionFailure"]exc["ExceptionPayload"]Converting Exception to Failure and Vice Versa
Exception and Failure objects are similar in their purpose of propagating errors with all accompanying error information. They are different in how they are propagated: Failure[…] is a return value, whereas Exception[…] is typically thrown rather than returned, i.e. propagated via nonlocal control flow.
It is important to be able to easily convert Exception objects to Failure objects and vice versa, so that propagated Exception can become a Failure return value, or Failure[…] can be turned into Exception[…] to be propagated nonlocally and bypass certain intermediate functions in the call stack.
For the purposes of this section, the following exception will be created and stored in a variable:
exc = Exception[abcWrongValueException, <|"Value" -> 200, "ExtraData" -> 42|>]Converting Exception to Failure
The built-in and most direct way to do this is to use the "ExceptionFailure" property of the Exception object:
exc["ExceptionFailure"]The important feature of this method is that the conversion is lossless. This means that one can reconstruct the original exception easily from such a Failure object, which for the purposes of this tutorial can be called Exception upgradeable.
Exception @ exc["ExceptionFailure"]If what you are given is not the Exception object itself but the exception data (an association), then the simplest way to convert that to an Exception upgradeable Failure object is to first (re)construct an Exception from this data.
excData = exc["ExceptionData"]Exception[excData]["ExceptionFailure"]One practically important use case for this method is to convert the caught exception directly into a Failure object, when catching an exception with CatchExceptions. This is exactly what the default exception handler does.
CatchExceptions[All] @ ThrowException[exc]CatchExceptions[All -> Function[Exception[#]["ExceptionFailure"]]] @ ThrowException[exc]By default, the tag of the resulting Failure object is the string version of the main exception's tag. It can be obtained with the "ExceptionFailureTag" property.
exc["ExceptionFailureTag"]You can redefine the value of this tag by providing your own value of the "ExceptionFailureTag" property when creating your exceptions.
excWithFTag = Exception[abcWrongValueException, <|"Value" -> 200, "ExtraData" -> 42, "ExceptionFailureTag" -> "CustomFailureTag"|>]excWithFTag["ExceptionFailure"]There are, of course, other ways to convert Exception objects to Failure objects. You can, for example, do that directly, using the exception data.
Failure["SomeError", KeyTake[exc["ExceptionData"], {"Value", "ExtraData"}]]Such methods, however, may lose some of the internal information about the exception, which may not allow for automatic back-conversion of such Failure objects to exceptions. More details on that are in the next subsection on the inverse conversion.
Converting Failure to Exception
This is a practically important task, particularly for cases when one wants to convert some Failure object (which, for example, was returned by some function call), into an Exception, to propagate it further.
There are two main ways to propagate the Failure object via exceptions. One is to make it a data payload of some exception. Another is to directly convert it to an equivalent exception. The latter requires either the Failure object to be Exception upgradeable (see previous subsection) or essentially to use Apply[Exception] on the Failure object, which is usually not the best way.
Using a Failure object as data payload is often the right thing to do. It makes it easy to access, manipulate and possibly return the propagated Failure when catching an exception with CatchExceptions.
excWithFailure = Exception[abcWrongValueException, Failure["SomeError", <|"Value" -> 200 |>]]Exception[abcWrongValueException, <| "Failure" -> Failure["SomeError", <|"Value" -> 200 |> ]|>]CatchExceptions[abcException -> Function[#ExceptionPayload]] @ ThrowException[excWithFailure]If you want to propagate Failure directly as an Exception (not as a payload of enclosing Exception), then in order to make the Failure object directly convertible to an Exception object, it needs to contain the "ExceptionTagList" or "ExceptionTag" properties. The Exception constructor will then recognize such a Failure object as Exception upgradeable.
Exception @ Failure["SomeError", <|"Value" -> 200, "ExceptionTag" -> abcWrongValueException|>]Exception @ Failure["SomeError", <|"Value" -> 200, "ExceptionTagList" -> {abcWrongValueException, abcException, "WrongValueException"}|>]If the "ExceptionTagList" key is present, it has to contain the full list of exception types (including the main type and its parent types), since the framework will make no attempt to expand the exception types in this case.
flawedExc = Exception @ Failure["SomeError", <|"Value" -> 200, "ExceptionTagList" -> {abcWrongValueException}|>]CatchExceptions[abcException] @ ThrowException @ flawedExcIf none of these keys are present in the failure's data, the Exception constructor cannot directly construct an exception out of such a Failure.
Exception[Failure["SomeError", <|"Value" -> 200 |> ]]As described earlier, you can pass Failure as a second argument to an Exception constructor, but then the Failure object becomes a data payload of the larger Exception object, nested inside the resulting Exception object, rather than being converted to it directly, which may or may not be what you want in a particular case.
Exception[abcWrongValueException, Failure["SomeError", <|"Value" -> 200 |> ]]Finally, one can directly construct Exception from Failure by "brute force", i.e. for example just by using Apply[Exception].
Exception @@ Failure["SomeError", <|"Value" -> 200 |> ]This will use the failure's tag as the exception type, which is a possible choice but often not the best one. In particular, error tags in Failure objects are usually strings, and whereas string types are supported in exceptions, one faces the uniqueness/name collision problems for them, which do not exist for symbolic exception types.
When Does It Make Sense to Return an Exception Object from a Function?
Exception[…] is not intended to be a regular return value, and this is where it is distinctly different from a Failure object. In fact, in most cases, Exception objects will be completely invisible. You can use the Exceptions framework without ever creating Exception objects explicitly.
var = func[ThrowException[abcWrongValueException, <|"Data" -> 42|>]]uncaught = %InputForm[uncaught]CatchExceptions[abcException -> Function[#Data]] @func @ ThrowException[abcWrongValueException, <|"Data" -> 42|>]The real reason to use the Exception constructor explicitly is to do something more advanced that would make use of the Exception object API. And the main and almost only reason to return Exception objects from your functions is to somehow prepare or customize the Exception object's construction for subsequent use in ThrowException.
createCustomException[data_] := Exception[abcWrongValueException, <|"CustomDataKey" -> data|>]ThrowException @ createCustomException[42]Another valid use case for using the Exception object as a value is when you attach it as a part of the information to some enclosing objects. For example, your project has a debug mode, in which you may want to return the exception you caught as a part of the information in the resulting Failure object. Or you throw a different exception, perhaps of a different type, but would like to keep the "original" Exception object somewhere in the information you propagate.
You should avoid returning Exception objects for other purposes, such as a return value for a failure, i.e. using them in places where Failure objects should be used.
Exception Types and Type Hierarchies
This section describes exception types in detail. You can skip some parts of it (e.g. type registration/hierarchies) on the first reading, but some parts of the subsequent sections rely on this material.
Exception Types
Exception types are one of the pillars of the Exceptions framework. They are useful because they allow one to break various types of errors into categories and propagate them differently, depending on their type. In particular, certain functions may be in a position to catch and handle exceptions of some types, while letting exceptions of other types pass through, until those reach the code that can handle them.
Here are a few important things to remember about exception types:
- One registers symbols as exception types, using RegisterExceptionType. It usually only makes sense to register a symbol if it will be a part of an exception type hierarchy.
- Any valid exception type (string or symbol, not necessarily registered) can serve as one of the parent types (can have subtypes).
The ability to form exception type hierarchies is also important in more advanced scenarios, where it may be convenient to catch entire groups of exception types with a single exception clause/handler.
Using Valid Exception Types
Examples below illustrate the use of various exception types.
ThrowException["IOError", 42]ThrowException[InvalidValueType, <|"Value" -> 42|>]RegisterExceptionType[FileReadingError, "IOError"]exc = Exception[FileReadingError, <|"FilePath" -> "abc"|>]{ExceptionQ[exc, FileReadingError], ExceptionQ[exc, "IOError"]}ThrowException[exc]CatchExceptions["IOError"]@ {1, 2, ThrowException[exc]}Symbolic Exception Types, Type Registration and Type Inheritance
Whereas it is possible to use any symbol (or string) as an exception type, one can also register symbolic exception types (but not string types), so that the system knows that the symbol represents an exception type.
Registering an Exception Type
You can use ExceptionTypes to get a list of all currently registered exception types.
ExceptionTypes[]To register an exception type, use the RegisterExceptionType function.
RegisterExceptionType[SquareIntegerException]MemberQ[ExceptionTypes[], SquareIntegerException]To check whether or not a given symbol represents a registered exception type, use ExceptionTypeRegisteredQ, which performs this check fast (in constant time).
ExceptionTypeRegisteredQ[ SquareIntegerException]Exception Type Hierarchies
From the practical viewpoint, the main reason to register an exception type is to build an exception type hierarchy. Each registered exception type may have zero, one or more parent types, which must be specified in the second argument of RegisterExceptionType.
There are two main reasons to create exception type hierarchies:
Creating an Exception Type Hierarchy
As mentioned, types being registered must be symbols. This applies, in particular, to the types that have parent types, since they (the child types) must always be registered. The parent types themselves are not generally required to be registered (unless they themselves must have parent types) and can be either symbols or strings.
If[!ExceptionTypeRegisteredQ[SquareIntegerException], RegisterExceptionType[SquareIntegerException]]The current version of the framework makes an assumption that the symbol being registered as an exception type is not Locked. In the process of registration, it clears all prior global rules associated with it (DownValues etc.), adds some new ones instead and finally, applies Protect to the symbol. So for exception types, it is best to use dedicated symbols without any global rules attached.
RegisterExceptionType[NonIntegerArgument, SquareIntegerException]RegisterExceptionType[WrongArgumentCount, {SquareIntegerException, "ArgumentCountException"}]One can use the two-argument form of ExceptionTypeRegisteredQ to test whether a certain symbol has been registered as a subtype of any of one or more exception types.
ExceptionTypeRegisteredQ[WrongArgumentCount, SquareIntegerException]ExceptionTypeRegisteredQ[WrongArgumentCount, "ArgumentCountException"]ExceptionTypeRegisteredQ[WrongArgumentCount, {SquareIntegerException, "SomeOtherExceptionType"}]ExceptionTypeRegisteredQ[WrongArgumentCount, "SomeOtherExceptionType"]ExceptionTypes[type] provides a simple way to find all subtypes of a given (not necessarily registered) exception type type.
ExceptionTypes[SquareIntegerException]ExceptionTypes["ArgumentCountException"]There is currently no documented way to find all direct parent types for a registered exception type that is guaranteed to work in future versions of the framework. One usually does not need this information programmatically, to use exceptions.
It is however possible, if not very intuitive, to get a list of all ancestor types for a type (including itself), by using an Exception object.
Exception[WrongArgumentCount]["ExceptionTagList"]Using Type Inheritance
The framework simplifies error handling by allowing CatchExceptions clauses to automatically catch exceptions of both a parent type and its derived child types. This opens several possibilities.
First, you can simply handle all exceptions in your hierarchy (or some sub-hierarchy) with a single handler.
The example below extends the previously defined function squareInteger to throw more than one exception type.
squareInteger[val_] := If[IntegerQ[val], val ^ 2, ThrowException[NonIntegerArgument, val]]
squareInteger[args___] := ThrowException[WrongArgumentCount, Length[{args}]]CatchExceptions[SquareIntegerException -> handler][squareInteger[Pi]]CatchExceptions[SquareIntegerException -> handler][squareInteger[1, 2, 3]]Second, you can still use subtype-specific clauses but then add the catchall clause with the parent type, to be sure that all of your exceptions are handled.
CatchExceptions[
squareInteger[Pi], { NonIntegerArgument -> handleNonintegerInput, SquareIntegerException -> handleAll}]CatchExceptions[
squareInteger[1, 2, 3], { NonIntegerArgument -> handleNonintegerInput, SquareIntegerException -> handleAll}]Scoping Your Exceptions
The previous section makes it clear how scoping of exceptions works:
- Each (sub)project or (sub)package typically introduces a symbol (in its main context or private subcontext) as its main exception type.
- The project may introduce intermediate types, which are subtypes of the main type but themselves have subtypes.
- In the simplest case, all exceptions of any of the subtypes can be caught by a single clause in CatchExceptions, with the main exception type.
How Does This Work?
The way the inheritance mechanism works with exception handling is completely straightforward. The Exception object is fully constructed by ThrowException (more precisely, by the Exception constructor, which ThrowException calls), and it contains full information about type inheritance, needed for the purposes of exception catching later by CatchExceptions.
What Exception constructor does is extremely simple: it just expands all the parent types for a given exception type into a flat list, which becomes the first part of the formed Exception object (the second part being the association with exception data).
Exception[WrongArgumentCount, 3]//InputFormClearAll[throwException, catchExceptions]
throwException[e_Exception ? ExceptionQ] := Throw[e, $tag]
SetAttributes[catchExceptions, HoldAll]
catchExceptions[expr_, tags_List -> handler_] :=
Replace[
Catch[expr, $tag, $magicHead],
$magicHead[e : HoldPattern @ Exception[etags_List, data_], _] :> If[
IntersectingQ[etags, tags], (* catching condition is very simple *)
handler[data],
throwException @ e
]
]catchExceptions[
throwException @ Exception[WrongArgumentCount, 3], {SquareIntegerException} -> handler]The important thing about this type-expansion mechanism is that it fires at the time when an exception is thrown, not when it is caught. This allows CatchExceptions to know absolutely nothing about the possible type hierarchy of the exception it is catching. All it needs is just a flat list of exception types—the type itself plus all of its ancestor types. And because all ancestor types are on that list, any clause in CatchExceptions that includes any of the parent types will automatically catch all exceptions of the subtypes, as the intersection of tags listed in the clause and tags present in the Exception object being caught will be non-empty.
Deregistering Exception Types
Exception type deregistration is not a common operation. Most projects would register their exception types and never ever need to deregister them. However, it is possible to do and may be occasionally useful: for example, to deregister some throwaway exception types defined during code experimentation and/or some interactive work.
All registered exception types become protected (acquire the Protected attribute) in the process of their registration. Before deregistering a registered type, two things must be ensured:
The first condition is a safety measure, since deregistering an exception type is a potentially destructive and irreversible operation that can break the error-handling functionality associated with this type. Deregistering exception types is closely analogous to calling Remove on symbols in the general Wolfram Language programming (where doing so may also subtly invalidate code that depends on the symbol being removed, which makes Remove also a specialized and rather rarely used tool).
Deregistering the type is carried out by calling DeleteObject on the registered exception type.
DeleteObject[SquareIntegerException]Unprotect[SquareIntegerException]
DeleteObject[SquareIntegerException]Unprotect[NonIntegerArgument, WrongArgumentCount]
DeleteObject[{NonIntegerArgument, WrongArgumentCount}]
DeleteObject[SquareIntegerException]Throwing and Catching Exceptions
This section gets into the details of using ThrowException and CatchExceptions to throw and catch exceptions.
To keep this section self-contained, the following code will be repeated from the previous sections.
RegisterExceptionType[NonIntegerArgument, SquareIntegerException];
RegisterExceptionType[WrongArgumentCount, SquareIntegerException];squareInteger[val_] := If[IntegerQ[val], val ^ 2, ThrowException[NonIntegerArgument, val]]
squareInteger[args___] := ThrowException[WrongArgumentCount, Length[{args}]]How Exceptions Are Propagated
Here is a description of the exception throwing and catching dynamics/algorithm:
- An Exception object is either passed to, or created by ThrowException, which then throws it.
- If the argument of ThrowException is a valid Exception object, it just throws it (technically, it uses Throw under the hood, with some custom tag, specific to the Exceptions framework and the same for all exceptions).
- Otherwise, the first argument of ThrowException must always be some exception type. ThrowException[type,args] first calls the constructor Exception[type,args] and then throws the resulting Exception object.
- Exception object is then propagated through the call stack, until it either reaches a CatchExceptions call that can catch it or exhausts the call stack and reaches the top level.
- Technically, each enclosing CatchExceptions[expr,spec] catches all exceptions thrown inside expr, but then rethrows all that do not match any of the clauses in spec. But for the end user, this detail is largely irrelevant, and one can think of non-matching exceptions as simply passing through.
- Exceptions that reach the top level are considered uncaught. In the context of software development in the Wolfram Language, uncaught exceptions should be considered bugs in the vast majority of cases. Each project should make sure to catch all of its internal exceptions in all of its user-facing functions.
- Each of the enclosing CatchExceptions tries all of its clauses against the set of types of the propagating Exception object (which typically includes the original type it was created with, together with all of its parent types).
- In case of a match, it catches the propagated exception (thereby stopping the propagation process) and passes its data to the handler function of the first matching clause.
One important feature of this mechanism is that all information about exception type inheritance (type hierarchies) is only used at the exception construction step. This is done by the Exception constructor, which builds a flat list of exception types (which includes the original type and its ancestor types) and includes that list as a part of the created Exception object.
In particular, CatchExceptions knows nothing about the type inheritance. It only looks at the set of types physically present in a given Exception object (which, like most other Wolfram Language expressions, is a stateless and immutable expression) and compares that with the types in its clauses to determine the fact of the match. In this way, CatchExceptions is fully decoupled from the type inheritance, and the entire exception propagation mechanism is completely stateless.
Internal Mechanics: Relation to Throw and Catch
The Exceptions framework is built on top of Throw and Catch. The purpose of this subsection is to explore this connection a little more deeply and show that there is nothing magical about how CatchExceptions and ThrowException work and that they are essentially quite simple operations.
The usual caveat applies here: anything about the internals may change in the future, so whereas it may be a good idea to understand how things work under the hood, it is strongly recommended to stick only to the documented functionality when using Exceptions framework in your code.
CatchExceptions vs. Catch
CatchExceptions is built on top of Catch. In the current version of the framework, it uses the same internal Catch tag to catch all propagating exceptions. This tag is not hard to determine:
$ErrorHandlingTag = Quiet @ Catch[ThrowException["Test"], _, #2&]One can use Catch with this tag to actually catch any Exception object thrown by ThrowException:
caught = Catch[squareInteger[1, 2, 3], $ErrorHandlingTag]One thing you may have noticed is that the uncaught exception message was still generated, which does not happen when one uses CatchExceptions:
properlyCaught = CatchExceptions[squareInteger[1, 2, 3], All -> Exception]where Exception constructor was used as the exception handler, to produce the result that is otherwise the same as previously.
There are a few other more subtle technical differences. For example, there is a system property "ExceptionUncaught", which is set to True in the former case and missing altogether in the latter:
#["ExceptionUncaught"]& /@ {caught, properlyCaught}One other consequence of CatchExceptions being built on top of Catch is that you can always catch any thrown exception with Catch[expr, _]:
func @ Catch[squareInteger[1, 2, 3], _]where func was used to illustrate that the exception has indeed been caught.
Of course, it is not recommended to use this knowledge in practice, i.e. to replace calls to CatchExceptions with calls to Catch[expr,_] or Catch[expr,$ErrorHandlingTag]. Apart from this not being portable between Wolfram Language versions, you will also miss things like built-in internal handler dispatch and selective exception catching, and you would have to manually rethrow all exceptions that are not of the type(s) you want to catch.
ThrowExceptions vs. Throw
Just like CatchExceptions is built on top of Catch, ThrowException is built on top of Throw. This means that technically you can imitate ThrowException by Throw[Exception[…],$ErrorHandlingTag]:
Throw[caught , $ErrorHandlingTag]A more faithful imitation can be produced by using the three-argument form of Throw:
func[Throw[caught, $ErrorHandlingTag, #&]]where func was used to illustrate that the Exception object has been thrown rather than returned.
You can indeed catch exceptions thrown in this way, with CatchExceptions:
CatchExceptions[All -> handler] @ Throw[caught, $ErrorHandlingTag, #&]But here also, like with the case of using Catch in place of CatchExceptions, you will miss certain parts of the standard/documented behavior. For example, ThrowException emits a very specific error message if an exception has not been caught by any enclosing CatchExceptions:
ThrowException @ caughtwhereas this message is not issued when using Throw directly.
Summary
This section illustrates how CatchExceptions and ThrowException are related to Catch and Throw, on a more technical level. This information aims to deepen your understanding of the framework.
It is, however, not suggesting to replace CatchExceptions with "equivalent" Catch calls, and/or ThrowException with "equivalent" Throw calls. Doing so would be a bad practice, which can lead to non-portable code and possibly various subtler issues and inconsistencies, and would be completely unjustified for the overwhelming majority of use cases.
Throwing Exceptions with ThrowException
You throw exceptions with ThrowException. There are a few general features of ThrowException to keep in mind:
- ThrowException never returns a value. It always throws some exception.
- When used incorrectly, ThrowException throws internal exceptions of type "ErrorHandlingException".
- ThrowException[args] is always equivalent to ThrowException @ Exception[args].
Because of the last point, the following sections do not explain the details of the various forms of ThrowException syntax—those were described in the section Constructing Exception Objects. You can directly translate all the forms of Exception constructor discussed there to equivalent syntax in ThrowException.
The Relationship between Exception and ThrowException
It is important to realize that ThrowException is, at least semantically, a very thin operation. Essentially, and this is literally true, the following always holds:
ThrowException[args___] := ThrowException @ Exception[args]This is not a metaphor, but a fundamental part of the framework's design. In particular, in the current version of the framework, one can check:
Last @ DownValues[ThrowException]What this means is that ThrowException delegates most of the work to the Exception constructor (which knows how to construct Exception objects from various forms of passed arguments) and then simply throws the constructed Exception object.
What this also means is that it is enough to study various forms of the Exception constructor Exception[args] to be able to effectively use ThrowException in all the similar ways, even if you never explicitly construct Exception objects, since ThrowException has exactly the same argument structure. The only nontrivial part that ThrowException does on its own is to actually throw a fully formed Exception object.
Examples of ThrowExceptions Typical Use Cases
Examples in this section will largely follow similar examples of the section Constructing Exception Objects, and are included here to keep this section self-contained. More details and explanations for them can be found in the aforementioned section.
ThrowException["SomeError"]ThrowException[SomeSymbolicException]ThrowException[NonIntegerArgument, "Value" -> Pi]ThrowException["SomeError", 42]ThrowException[<|"ExceptionTagList" -> {"SomeError"}, "Data" -> 42|>]ThrowException@ Failure["SomeError", <|"ExceptionTag" -> NonIntegerArgument, "Value" -> Pi|>]ThrowException[Exception[ "SomeError", 42], <|"ExtraData" -> 100|>]For more details and explanations of all such use cases, please read the section Working with Exception Objects. Some of the less common use cases are also discussed in the subsections below.
Throwing Different Types of Exceptions
You can use several exception types for the exceptions you throw in your code. For complex or large projects, this arguably is a better practice in general than using just a single exception type throughout the project.
The example below illustrates how the function squareInteger throws exceptions of different types for different kinds of errors.
squareInteger[Pi]squareInteger[1, 2, 3]In this particular case, both types of thrown exceptions are subtypes of a single type SquareIntegerException, which is the root exception type of the exception type hierarchy created for this example.
Throwing and Rethrowing Exceptions in Exception Handlers
One particular workflow that is often useful in practice is to rethrow the caught exception from within the exception handler, possibly under some condition, and in some cases with some extra added information.
The simplest way to rethrow the caught exception is to use the low-level Exception constructor that takes the exception data (an association) directly, or the equivalent variant of ThrowException.
catchConditionally = CatchExceptions[NonIntegerArgument -> Function[
With[{e = Exception[Echo @ # ]},
If[#ExceptionPayload === Pi,
ThrowException[e],
(* else *)
e["ExceptionFailure"]
]]]
];catchConditionally @ Map[squareInteger, {1, 2, 1.4142135}]catchConditionally @ Map[squareInteger, {1, 2, Pi}]You can obviously also throw "brand new" exceptions, possibly of different types, from within exception handlers. In this context, the code inside exception handlers is no different from any other code, where you may want to throw exceptions under certain conditions.
It is important to keep in mind that the exception (re)thrown inside an exception hander will not be caught by the same CatchExceptions that rethrows it (even if it contains matching clauses) and will require another CatchExceptions with the matching clause in the enclosing code, to be caught.
Another important use case arises when an exception handler contains code that may throw an exception, and you, to the contrary (as compared to the previous examples of this section), do not want that exception to propagate further. In such cases, you have to wrap that code with a call to CatchExceptions, inside your exception handler. That case will be further discussed in the section Catching Exceptions With CatchExceptions.
Throwing Exception-Upgradeable Failure Objects
As discussed in the chapter on the Exception object, Exception upgradeable Failure objects are those that can be automatically converted to Exception objects and thrown directly by ThrowException, which is often very convenient. To be Exception upgradeable, a Failure object must contain either the key "ExceptionTagList" with a value being a list of tags, or the key "ExceptionTag" with a value being an exception type, or both.
Creating an Exception upgradeable object is particularly easy when one can use Exception constructor. This has been discussed in the section Converting Exception to Failure and Vice Versa, but examples below also show how to construct them, to keep this section self-contained.
Failure["ArgumentError", <|"ExceptionTag" -> NonIntegerArgument, "ArgumentValue" -> Pi|>]ThrowException[%30]ThrowException @ Failure["ArgumentError", <|"ArgumentValue" -> Pi|>]ThrowException[NonIntegerArgument, Failure["ArgumentError", <|"ArgumentValue" -> Pi|>]]Whenever your internal functions should return Failure objects rather than throw exceptions (for whatever reasons), it is recommended to make those Failure objects Exception upgradeable, so that they can be easily converted to exceptions and thrown by any enclosing internal function.
(Re)Throwing Exception Objects with Additional Types and/or Extra Information
This is an advanced use case, where you might want to augment an existing Exception object with some extra data and/or throw it with some extra exception type. There are a few scenarios where this may be useful:
- You have written a function that prepares the "core" Exception object, which is then augmented with different additional data, depending on the use case.
- You have caught some exception and want to rethrow it with certain extra information, for example, containing some extra evaluation details and/or the name of the intermediate intercepting function.
- You have caught an exception of some generic type (perhaps not even coming from your package, although such cases should be rare), and you want to add to it some exception type local to your package and rethrow it, so that you can catch it in your user-facing functions, using the root exception type of your package.
There is a simple way to add extra data or even an extra exception type to an existing exception, using an Exception object copy constructor (described in more detail in the subsection Constructing Exception Objects) or the equivalent ThrowException syntax.
The example below illustrates the first use case.
niaException[val_] := Exception[NonIntegerArgument, "ArgumentValue" -> val]Exception[niaException[Pi], <|"Function" -> func|>]ThrowException[%35]ThrowException[niaException[Pi], <|"Function" -> func|>]The second case (exception rethrowing) was covered in the previous subsection, and you can use a similar approach, but also use there Exception copy constructor, just like here. That case is simpler, however, since exception handlers are passed all exception data directly as an association, and you can just append your data to it with Append and use that to construct a new exception.
The last case (adding more types to an existing exception) is somewhat more interesting and less trivial.
If your task is to rethrow some caught exception but with some added exception type, perhaps local to your package (say its name is MyPackageException, for the sake of example), there are several ways to go about that. One simple way is to convert the original exception to Failure, and use that Failure as a payload of a new exception—and in many cases this may be what you need. This is shown in the next example.
CatchExceptions["IOException"]@ ThrowException["IOException", <|"FilePath" -> "abc"|>]ThrowException[MyPackageException, %38]If you want to avoid the nesting associated with the previous method, there is another possibility. It is based on the feature of the Exception constructor of merging the outer exception with the inner Exception object, in cases when the latter is passed as a payload (second argument). In such cases, the types are also merged in the new Exception object. This is illustrated in the next example.
Exception[MyPackageException, Exception["IOException", <|"FilePath" -> "abc"|>], <|"ExtraData" -> 100|>]CatchExceptions["IOException" -> Function[ThrowException[MyPackageException, Exception[#], <|"ExtraData" -> 100|>]]]@ ThrowException["IOException", <|"FilePath" -> "abc"|>]CatchExceptions["IOException" -> Function[ThrowException[MyPackageException, Exception[Append[#, <|"ExtraData" -> 100|>]]]]]@ ThrowException["IOException", <|"FilePath" -> "abc"|>]Adding more types to existing exceptions can be convenient in various scenarios. For example, MyPackageException is your "root" exception type. You may want to propagate some exception of a different type, caught in some of your inner functions, up the execution stack, but be sure to catch it in your user-facing functions by the same exception handler you use for exceptions of MyPackageException type and its subtypes. Such exceptions can be thought of as a mix-ins.
Incorrect Uses of ThrowException, and The Internal Exceptions of Exceptions Framework
ThrowException always throws some Exception object but not necessarily the one you intended to throw. If your exception is malformed in some way, ThrowException will throw instead a different kind of exception, of the string type "ErrorHandlingException"—which is the type for internal exceptions thrown by the Exceptions framework itself.
One scenario when this happens is when one tries to use ThrowException without an exception type.
ThrowException[42]Exception[42]Interestingly, misusing ThrowException also creates a situation where Exception and ThrowException show some discrepancy, or it may so appear. In some such cases, Exception stays inert, whereas ThrowException must always throw some exception, simply by its contract.
Exception[1, 2, 3]ThrowException @ Exception[1, 2, 3]Exception @ Exception[1, 2, 3]Catching Exceptions with CatchExceptions
One catches thrown Exception objects with CatchExceptions. The spec in CatchExceptions[code, spec] can be described by the following BNF grammar:
spec ::= singleClause | {singleClause...}
singleClause ::= fullTypeSpec | (fullTypeSpec -> handlerSpec)
fullTypeSpec ::= typeSpec | {typeSpec...}
typeSpec ::= All | Symbol | String
handlerSpec ::= Automatic | function
where "function" is any Wolfram Language expression representing a function of a single argument, and an appearance of fullTypeSpec without an explicit handler is interpreted by the system as fullTypeSpec -> Automatic in any given clause of CatchExceptions.
There are a few things to remember about CatchExceptions, some of which follow from the above grammar:
- You have to indicate at least one exception type to catch, or All to catch exceptions of any type (the use of All is generally not recommended but has its place as, e.g. catchall clause in CatchExceptions).
- For a particular exception clause, in the absence of an explicit exception handler, the system will use a default one.
- The default exception handler is currently equivalent to Function[Exception[#]["ExceptionFailure"]] and thus returns an (Exception upgradeable) Failure object, but this may change in future versions.
- You can catch exceptions of multiple types and handle them all with the same handler by using CatchExceptions[{type1,type2,…}handler].
- If exception types form a hierarchy, you can use the root exception type to automatically catch all exceptions of the subtypes.
- CatchExceptions will not catch exceptions (re)thrown from within the exception handling code of one of its exception handler clauses.
The following subsections illustrate some of these points and a few extra ones, with examples.
Handling Multiple Exception Types With a Single Exception Handler
There are several ways one can catch exceptions of multiple exception types. The simplest option is to use the same error handler for all of the types; then one just needs to put them in a list.
CatchExceptions[squareInteger[Pi], {NonIntegerArgument, WrongArgumentCount} -> handler]CatchExceptions[squareInteger[1, 2, 3], {NonIntegerArgument, WrongArgumentCount} -> handler]CatchExceptions[squareInteger[Pi], {NonIntegerArgument, WrongArgumentCount}]CatchExceptions[squareInteger[1, 2, 3], {NonIntegerArgument, WrongArgumentCount}]Handling Multiple Exception Subtypes of a Single Parent Type
One of the main advantages of having exception type inheritance hierarchies is that you can catch groups of related exceptions with a single CatchExceptions clause. This case has been already discussed several times in this tutorial, but it will be repeated here for completeness and illustrated with additional examples.
CatchExceptions[squareInteger[Pi], SquareIntegerException -> handler]CatchExceptions[squareInteger[1, 2, 3], SquareIntegerException -> handler]Because the specific exception type of the caught exception is available to you via the "ExceptionTag" key of the data passed to the handler, you can, if you wish, engineer some internal dispatch for the subtypes.
The following example illustrates how you can incrementally build your own internal type-based dispatch in a modular fashion, using powerful functional programming abstractions available in the Wolfram Language, such as higher-order functions, operator forms and function composition.
catchallHandlerForType[type_] := Composition[
Function[#["ExceptionFailure"]],
Exception,
Echo,
Append[
<|"MessageTemplate" -> "Unknown error of type ``", "MessageParameters" -> {type}|>
]
]CatchExceptions[NonIntegerArgument -> catchallHandlerForType[NonIntegerArgument]] @ squareInteger[Pi]$customDispatch = Replace[{
NonIntegerArgument :> firstHandler,
WrongArgumentCount :> secondHandler,
errType_ :> catchallHandlerForType[errType]
}];{$customDispatch[NonIntegerArgument], $customDispatch[SomeSymbolicException]}customCatch = CatchExceptions[ SquareIntegerException -> ($customDispatch[#ExceptionTag][#]&)] ;customCatch @ squareInteger[Pi]customCatch @ squareInteger[1, 2, 3]customCatch @ {1, 2, ThrowException[SquareIntegerException], 4, 5}The built-in type dispatch mechanism of CatchExceptions, discussed below, frequently offers simpler ways to organize the type-based dispatch and is generally the recommended way to accomplish that. But the above method represents a perfectly valid usage pattern too.
Handling All Exception Types Using All in CatchExceptions
One can use All in place of exception type(s) in the CatchExceptions handler spec. That will catch exceptions of all types.
CatchExceptions[squareInteger[Pi], All -> handler]CatchExceptions[squareInteger[1, 2, 3], All -> handler]CatchExceptions[squareInteger[Pi], All]CatchExceptions[squareInteger[1, 2, 3], All]The use of All is generally not recommended, however, unless you must ensure that no exception leaves your code and you do not care about possibly swallowing exceptions that your code did not throw. There is a tricky balance here between not letting any exceptions be thrown by your public functions and not swallowing errors your code is neither responsible for nor able to properly handle.
Using Different Handlers for Different Exception Types
In more complex cases, you may want different exception types to be handled differently. One way to do that has been described earlier and involved manual building of type-based dispatch inside a single exception handler associated with the root/parent exception type.
Another, often simpler way, is to use the syntax CatchExceptions provides for multiple exception handlers. The following examples illustrate the use of different exception handlers for different exception types, using built-in dispatch mechanism of CatchExceptions.
CatchExceptions[squareInteger[Pi], {NonIntegerArgument -> firstHandler, WrongArgumentCount -> secondHandler}]CatchExceptions[squareInteger[1, 2, 3], {NonIntegerArgument -> firstHandler, WrongArgumentCount -> secondHandler}]CatchExceptions[squareInteger[Pi], {NonIntegerArgument -> firstHandler, WrongArgumentCount -> Automatic }]CatchExceptions[squareInteger[1, 2, 3], {NonIntegerArgument -> firstHandler, WrongArgumentCount -> Automatic}]The Order of the Exception-Handling Clauses: Which Handler Will Handle Your Exception?
There can be cases when more than one exception-handling clause inside CatchExceptions matches the exception being caught. In this case, the first matching clause is used.
The framework makes no attempt to somehow reorder the clauses based on their specificity: even the more general clause will be used, if it comes before a more specific one. This is very much like ReplaceAll with a list of rules: the first matching one is used.
CatchExceptions[squareInteger[Pi], {All -> allHandler, NonIntegerArgument -> firstHandler, WrongArgumentCount -> secondHandler }]CatchExceptions[squareInteger[1, 2, 3], {All -> allHandler, NonIntegerArgument -> firstHandler, WrongArgumentCount -> secondHandler }]Catchall Exception Handlers and Functions Bulletproofing
It often makes sense to include some catchall exception handling clause(s) in your CatchExceptions calls. This is particularly relevant for the CatchExceptions calls used to catch all internal exceptions in the outer, user-facing functions.
There are at least two good reasons to do that:
- It is easy to forget to catch exceptions of some particular types, especially if your project has many exception types and/or as a regression when you add new types or change existing ones.
- It may happen that some functions your code imports from other packages throw some exceptions of entirely different types.
If your package has an exception type hierarchy with some root type, and you make sure that all your exception types are subtypes of that type, then you may want to place that type in the last clause in your CatchExceptions calls for your user-facing functions, after all of the more type-specific clauses.
CatchExceptions[squareInteger[1, 2, 3], {NonIntegerArgument -> firstHandler, SquareIntegerException -> packageHandler}]You can also prevent your code from leaking any other exceptions by adding another clause that would catch all other types of exceptions.
CatchExceptions[ThrowException["SomeOutsideError"], {NonIntegerArgument -> firstHandler, SquareIntegerException -> packageHandler, All -> allHandler}]Most Wolfram Language users do not expect the functions they use—aside from Throw and ThrowException—to propagate exceptions. From this viewpoint, all exceptions should likely be caught within your package's user-facing functions, even if they originate in external code (which would typically indicate an oversight or bug in the imported package).
Admittedly, requiring all exceptions to be caught at every package boundary somewhat limits their power. These requirements could perhaps be lifted for internal packages within a larger project—where the "users" are you or your fellow developers—and where the use of exceptions is expected and standardized through internal documentation or agreement.
Ultimately, the degree to which you bulletproof your functions against exception leaks is up to you and will likely depend on the specific use case. What matters is that the question of whether or not to catch external exceptions is addressed, even if the decision is to let them pass through.
This topic is further discussed in the next section, in the context of internal exceptions thrown in some cases by the Exceptions framework itself.
Conditional Catching of Internal (and Other) Exceptions
As noted in the section on ThrowException, it always throws some exception. But in cases when it is used incorrectly, that would be an internal exception of (string) type "ErrorHandlingException".
ThrowException[1, 2, 3]There are two choices you have regarding these internal exceptions:
- Use a separate clause in CatchExceptions, or a separate CatchExceptions call, to catch them.
Exactly which choice to make depends on the case at hand.
On one hand, any leaking exception from your user-facing functions should probably be treated as a bug on your side that needs to be fixed. On the other hand, the emergence of such exceptions should mean that you made some mistake in using ThrowException, and it is best if such a mistake manifests itself most explicitly, rather than being swallowed. And fixing that bug would mean fixing those incorrect calls, rather than silencing these exceptions.
If you decide to always catch such internal exceptions, there is also a chance that you may catch an internal exception thrown by some of the other packages or projects that your code depends on, but that you are not the author of (because the type "ErrorHandlingException" is a general string exception type, not scoped to your project). This would mean a bug on the side of those dependencies. You probably still want to catch those exceptions, but it is good to keep this in mind.
If your project has a debug mode, then one possible choice here is to catch these exceptions in the production mode but not in the debug mode.
Examples below illustrate these ideas.
$debugMode = False
exceptionHandler := If[!TrueQ[$debugMode],
CatchExceptions[{
SquareIntegerException -> handler, "ErrorHandlingException" -> Automatic}
],
CatchExceptions[SquareIntegerException -> handler]
]squareIntegerBad[val_] := If[IntegerQ[val], val ^ 2, ThrowException[NonIntegerArgument, val]]
squareIntegerBad[args___] := ThrowException[Length[{args}]]exceptionHandler @ squareIntegerBad[1.5]exceptionHandler @ squareIntegerBad[1, 2, 3]Block[{$debugMode = True}, exceptionHandler @ squareIntegerBad[1, 2, 3]]In fact, you may customize your exception handlers to also apply similar conditional logic to other types of your exceptions besides the internal exceptions of "ErrorHandlingException" type.
It is less recommended, but you can also use All for the catchall type (instead of "ErrorHandlingException" in this case) to catch all exceptions.
Passing Exceptions Through
There frequently happen situations where only certain exceptions should be caught, whereas others should be just passed through. This can be required, for example, for some internal functions, which are able to handle some exceptions but should send the others upstream. Or a package might be divided into subpackages, such that "public" functions in the subpackages are still considered internal for the whole project. In which case, they might want to catch the internal subpackage-level exceptions, but can still propagate the main project-level exceptions.
There are no additional steps one should take to enable that, except for just not catching those exceptions that should not be caught at a particular level.
CatchExceptions[squareInteger[1, 2, 3], WrongArgumentCount -> handler ]CatchExceptions[squareInteger[Pi], WrongArgumentCount -> handler ]CatchExceptions[ NonIntegerArgument -> someHandler ] @ Module[
{a = 1, result},
Print["Before calling squareInteger"];
result = CatchExceptions[
a + squareInteger[Pi],
(* Return 0 if squareInteger is called with wrong arg. count *)
WrongArgumentCount -> Function[0]
];
Print["After calling squareInteger"];
result
]Catching Exceptions Thrown in Exception Handlers
In some of the previously considered use cases, exception handlers decided to throw their own exceptions or rethrow those exceptions they have caught. In this section, the different, and in some sense opposite case is discussed: what to do with exceptions possibly thrown by the code inside exception handlers.
There are three somewhat distinct sets of cases to distinguish here:
- Exception handler explicitly throws or rethrows an exception. In this case, the intent is obvious, and this new exception is supposed to propagate further. This was discussed previously.
- Some intermediate function called as a part of evaluation of exception handler's code throws an exception, but this exception is intended to propagate further, similarly to the first case.
- Some intermediate function called as a part of evaluation of the exception handler's code throws an exception, but the exception must be caught and should not propagate further.
The first two cases do not need much discussion, but the last one needs some.
As a reminder, CatchExceptions does not catch exceptions thrown in exception handlers of that same CatchExceptions. Thus, there are two ways to ensure that exceptions are caught in this case:
- To wrap another CatchExceptions around the original CatchExceptions call.
- To wrap CatchExceptions around the code of the exception handler.
Examples below demonstrate and compare both these options.
ensureSmallInt[val_, maxSquareValue_ : 1000] := If[val ^ 2 > maxSquareValue, ThrowException["overflow", val], val]Map[ensureSmallInt, {1, 3, 7, 15 * Pi}]CatchExceptions["overflow" -> Function[Failure["overflow", <|"Number" -> #ExceptionPayload, "NumberSquared" -> squareInteger[#ExceptionPayload]|>]]]@Map[ensureSmallInt, {1, 3, 7, 15 * Pi}]CatchExceptions["overflow" -> Function[Failure["overflow", <|"Number" -> #ExceptionPayload, "NumberSquared" -> CatchExceptions[SquareIntegerException] @ squareInteger[#ExceptionPayload]|>]]]@Map[ensureSmallInt, {1, 3, 7, 15 * Pi}]CatchExceptions[SquareIntegerException] @CatchExceptions["overflow" -> Function[Failure["overflow", <|"Number" -> #ExceptionPayload, "NumberSquared" -> squareInteger[#ExceptionPayload]|>]]]@Map[ensureSmallInt, {1, 3, 7, 15 * Pi}]Whether and exactly how to handle exceptions originating in exception handlers is up to the developer and will be case dependent. What matters is to make sure those potential exceptions are either handled somehow or intentionally not handled, but not overlooked.
A Complete Example
This section illustrates the main features of the Exceptions framework in combination with a small but complete and realistic example of a package utilizing exceptions. The main function FileShortName computes the relative file name of a file w.r.t. some root path. The package registers one main exception type and three subtypes.
The internal implementation function fileShortName[…] does all the heavy lifting and throws various types of exceptions in cases of errors. The main function catches those exceptions, using the built-in dispatch mechanism of CatchExceptions to invoke type-specific exception handlers, which convert the exception data into appropriate error messages and the resulting Failure object.
In real applications, the inner functions that throw exceptions will be further separated from the public functions that call them by the intermediate functions, rather than being called directly, as is done here.
BeginPackage["PathUtilities`"]
ClearAll[FileShortName]
FileShortName::usage = "FileShortName[path_, file_] returns file's short name relative to path";
FileShortName::incns = "The file `2` is not in the directory `1`";
FileShortName::invdir = "The path `` is not a valid directory";
FileShortName::interr = "Internal error";
Begin["`Private`"];
RegisterExceptionType[PathUtilitiesException];
RegisterExceptionType[FileNotInPath, PathUtilitiesException];
RegisterExceptionType[InvalidDirectory, PathUtilitiesException];
RegisterExceptionType[WrongArgCount, PathUtilitiesException];
(* Helper function to be used for exception handling *)
SetAttributes[failureMessage, HoldFirst]
failureMessage[mn_MessageName, params___] := (
Message[mn, params];
Failure[
"PathUtilitiesError",
<|"MessageTemplate" :> mn, "MessageParameters" -> {params}|>
]
);
(* Main function *)
FileShortName[args___] := CatchExceptions[{
InvalidDirectory -> Function[failureMessage[FileShortName::invdir, #RootPath]],
FileNotInPath -> Function[failureMessage[FileShortName::incns, #RootPath, #FilePath]],
WrongArgCount -> Function[failureMessage[General::argrx,FileShortName, 2, #ArgCount]],
PathUtilitiesException -> Function[failureMessage[FileShortName::interr]]
}] @ fileShortName[args];
(* Implementation *)
fileShortName[_, root_] /; !StringQ[root] || !DirectoryQ[root] := ThrowException[InvalidDirectory, <|"RootPath" -> root|>];
fileShortName[file_String, root_String] := Replace[
{FileNameSplit[root], FileNameSplit[file]},
{
{{common__}, {common__, rest___}} :> FileNameJoin[{rest}],
_ :> ThrowException[FileNotInPath, <|"RootPath" -> root, "FilePath" -> file|>]
}
];
fileShortName[args___] := ThrowException[WrongArgCount, <|"ArgCount" -> Length[{args}]|>];
End[];
EndPackage[]
FileShortName[FindFile["ExampleData/ecommerce-database.sqlite"], $InstallationDirectory]FileShortName[FindFile["ExampleData/ecommerce-database.sqlite"], $UserBaseDirectory]FileShortName[FindFile["ExampleData/ecommerce-database.sqlite"], 42]FileShortName[]Below is an alternative implementation of the same function, where there is a single exception handler, and the type-based dispatch happens already inside of it and is implemented manually. The only part of the code that changes is the implementation of the main function, FileShortName. You can check that the new code results in exactly the same behavior as before, by running previous examples.
BeginPackage["PathUtilities`"]
ClearAll[FileShortName]
FileShortName::usage = "FileShortName[path_, file_] returns file's short name relative to path";
FileShortName::incns = "The file `2` is not in the directory `1`";
FileShortName::invdir = "The path `` is not a valid directory";
FileShortName::interr = "Internal error";
Begin["`Private`"];
RegisterExceptionType[PathUtilitiesException];
RegisterExceptionType[FileNotInPath, PathUtilitiesException];
RegisterExceptionType[InvalidDirectory, PathUtilitiesException];
RegisterExceptionType[WrongArgCount, PathUtilitiesException];
(* Helper function to be used for exception handling *)
SetAttributes[failureMessage, HoldFirst]
failureMessage[mn_MessageName, params___] := (
Message[mn, params];
Failure[
"PathUtilitiesError",
<|"MessageTemplate" :> mn, "MessageParameters" -> {params}|>
]
);
(* Main function *)
FileShortName[args___] := CatchExceptions[
PathUtilitiesException -> Function[
Replace[#ExceptionTag, {
InvalidDirectory :> failureMessage[FileShortName::invdir, #["RootPath"]],
FileNotInPath :> failureMessage[FileShortName::incns, #["RootPath"], #["FilePath"]],
WrongArgCount :> failureMessage[General::argrx,FileShortName, 2, #["ArgCount"]],
_ :> failureMessage[FileShortName::interr]
}]
]
] @ fileShortName[args];
(* Implementation *)
fileShortName[_, root_] /; !StringQ[root] || !DirectoryQ[root] := ThrowException[InvalidDirectory, <|"RootPath" -> root|>];
fileShortName[file_String, root_String] := Replace[
{FileNameSplit[root], FileNameSplit[file]},
{
{{common__}, {common__, rest___}} :> FileNameJoin[{rest}],
_ :> ThrowException[FileNotInPath, <|"RootPath" -> root, "FilePath" -> file|>]
}
];
fileShortName[args___] := ThrowException[WrongArgCount, <|"ArgCount" -> Length[{args}]|>];
End[];
EndPackage[]
These two code versions illustrate the freedom that the framework provides, in terms of which things should happen where. The reason the code in the second version of FileShortName works is, of course, the exception type inheritance, which allows a single generic exception handler to catch and handle all subtype exceptions.
However, the first method is the recommended one, since then you can use the built-in exception type dispatch mechanism.
Resource Management
This section covers several advanced but practically important use cases where exception handling becomes intertwined with resource management. In the Wolfram Language, managing resources in the context of exceptions is sufficiently complex to merit its own dedicated section.
This section is more technical and advanced than most other sections of this tutorial. It will be useful not only to the users of the Exceptions framework but also to those who use Throw and Catch as their primary tools for error propagation or who use hybrid workflows involving both.
What Resource Management Is, and Why This Section Is Needed
It is often important to ensure that certain cleanup code executes at specific levels, even when nonlocal control flow is in effect. This typically involves resource management tasks, such as closing files and database connections or invoking destructors. In the context of exceptions, many languages utilize the try-catch-finally triad, where the "finally" clause contains code that executes regardless of whether an exception was caught or propagated further.
While CatchExceptions could, in principle, support a third argument for a finally clause, it currently does not (though this may change in the future). Instead, the Wolfram Language provides a dedicated function, WithCleanup, which serves as a general-purpose tool for ensuring code execution during cleanup.
Two factors make this section both important and necessary:
- The interaction between Throw and WithCleanup, while consistent, creates a fundamental challenge for developers in certain scenarios. As discussed in detail below, this is a problem that must be addressed to ensure all Throw events are caught reliably.
- The current version of the Exceptions framework is somewhat limited in this regard, lacking comprehensive "out-of-the-box" functionality for resource management. This section aims to fill that gap.
The Logic and Summary of This Section
Since this section is rather long and technical, here is the summary for the impatient.
The straightforward use of WithCleanup in combination with either Throw/Catch or ThrowException/CatchExceptions, is not safe in general. However, there are ways to use WithCleanup safely with exceptions. The primary approaches are:
- Ensure that the main and the cleanup code inside WithCleanup never throw exceptions simultaneously.
- Replace the use of bare WithCleanup with a custom solution—such as the one discussed below—that solves this problem for you. Alternatively, you can write your own if your logic requires a different approach.
- First, analyze and explain the problems that arise when using WithCleanup in conjunction with Throw or ThrowException.
- Next, present the design and implementation of a custom solution intended to replace WithCleanup in this specific context.
WithCleanup, Throw and Catch: Multiple Propagating Exceptions Problem
The main documented resource management tool in the Wolfram Language is the WithCleanup function. It has two-argument and three-argument forms, but for the purposes of this tutorial, only the two-argument form will be considered: WithCleanup[code,cleanupCode]. it guarantees that the cleanup code will run even for the events that interrupt the control flow of the main code in a nonlocal way. Such events include exceptions, aborts, interrupts, calling Return nonlocally, etc. In such cases, the nonlocal control flow transfer is paused by WithCleanup until after the cleanup code executes.
Both in the more specific context of exceptions and in the more general one of Throw and Catch (on top of which the Exceptions framework is built), WithCleanup has one particular behavior that is problematic and has profound implications for robust exception handling.
Problem #1: The Need for Multiple Catch/CatchExceptions Calls
The case in question is when in both the main and cleanup sections of WithCleanup uncaught Throw events are present. In this case, both Throw events are propagated at the same time. This might be the only situation in the Wolfram Language when such behavior becomes at all possible. What this roughly means is that a single Catch (or CatchExceptions) call will not be enough to catch both events.
The following examples illustrate the problem.
WithCleanup[Throw[$Failed, "Main"], Throw[$Failed, "Cleanup"]]Catch[WithCleanup[Throw[$Failed, "Main"], Throw[$Failed, "Cleanup"]], _]Catch[Catch[WithCleanup[Throw[$Failed, "Main"], Throw[$Failed, "Cleanup"]], _], _]This behavior may go against the usual intuition one may have for Catch, which is that Catch[expr,_] should be able to catch any Throw event created by Throw[something,someTag].
The situation becomes worse if one starts to nest WithCleanup calls.
WithCleanup[Throw[$Failed, "PreMain"], WithCleanup[Throw[$Failed, "Main"], Throw[$Failed, "Cleanup"]]]Catch[Catch[WithCleanup[Throw[$Failed, "PreMain"], WithCleanup[Throw[$Failed, "Main"], Throw[$Failed, "Cleanup"]]], _], _]Catch[Catch[Catch[WithCleanup[Throw[$Failed, "PreMain"], WithCleanup[Throw[$Failed, "Main"], Throw[$Failed, "Cleanup"]]], _], _], _]It is clear that this logic extends to higher levels of nesting of WithCleanup. And this is not a feature specific to the Exceptions framework, it manifests itself on the more fundamental language level, that of Throw and Catch.
Moreover, in general, you would not even know how many levels of Catch (or CatchExceptions) you may need to reliably catch all Throw events created in such a regime. And since these nested WithCleanup calls do not have to be in the same function or package, such analysis may generally be quite complex or not at all feasible. Clearly, some other solution is needed here.
Problem #2: Control Flow Stealing and Information Loss
The problem with the multiple Throw events propagation mode goes further than just necessitating multiple Catch clauses to completely catch them all. Consider the following (rather counterintuitive) behavior of WithCleanup, Throw and Catch:
Catch[WithCleanup[Throw["Main", $main], Throw["Cleanup!", $cleanup]], $main, Print]Catch[WithCleanup[Throw["Main", $main], Throw["Cleanup!", $cleanup]], $main, Throw[##]&]So, the function of the inner Catch is never called, because the control flow is stolen by the second (uncaught) propagating Throw event (Throw["Main", $main] in this case), immediately after the first propagating event has been caught.
What this means is that the special mode of double (or multiple) propagating Throw events, which WithCleanup can create, is inherently flawed in the sense that it is not possible to catch both propagating events without a loss of some information: the information will be lost for whichever Throw event(s) you catch first, since the control flow will be immediately stolen by remaining propagating Throw events.
To rephrase a little, what this also means is that the inner Throw events, despite having been caught, cannot really be properly handled. If someone has custom Throw handlers of the form Catch[expr, someTagOrPattern, customHandler]—which work perfectly in the standard, single Throw event propagation mode, these handlers will not work as expected for multiple propagating Throw events, even when properly nested. They might catch relevant Throw events, but the handlers for internal ones will not be invoked, only for the outermost one, and so the information about the caught inner Throw events is therefore lost.
The situation with ThrowException, CatchExceptions and WithCleanup is exactly the same, because ThrowException is based on Throw, and CatchExceptions is based on Catch.
Conclusions
The implications for exception handling using WithCleanup are as follows:
- Whatever you do, you should probably never allow WithCleanup to get into the multiple Throw events propagation mode, because it is impossible to handle it without some information loss. That refers not just to exceptions, but to general Throw events, tagged or untagged.
- One safe but opinionated choice is to catch all Throw events (exceptions or not) in the cleanup section of WithCleanup, so that you can have at most one propagating Throw event after the code leaves WithCleanup.
The following section shows one possible way to automate the above recommendations.
Custom Solution for Deterministic Cleanup: Safeguarding the Propagation Path
This section presents one possible way to reduce the complexity of the previously described situation and automate the error handling there. The function withExceptionalCleanup, implemented and illustrated in this section, is a more constrained and opinionated extension/modification of WithCleanup, which in the context of exceptions and general Throw events has a more deterministic behavior. It excludes the mode of simultaneous propagation of multiple Throw events, and the associated with that mode problems of multiple Catch calls and information loss.
You can use withExceptionalCleanup directly in your code in places where you would normally use WithCleanup. It can also serve as a nontrivial example of the design and implementation of error-handling functionality. You can reuse its design logic and implementation patterns to implement your own versions of such functionality, better fitting your particular use case and code logic, if you need a yet more custom solution.
Specification
The suggested function withExceptionalCleanup has the following spec:
- Compared to WithCleanup, there is an extra List around the pair of code and cleanupCode.
- The main code section is executed first, with all Throw events (exceptions, tagged and untagged Throw events) caught and the result stored internally.
- The cleanup code section is executed next, with all Throw events (exceptions, tagged and untagged Throw events) caught and the result stored internally.
- If a custom cleanup exception handler has been passed via "CleanupEventHandler" option, it is applied to the result as handler[eventType, payload], where event types and payload values are described next.
- The resulting cleanup code evaluation information is formed:
<| "RawResult"…,"EventType"…,"HandledResult"…|> - The main code handler is applied to the main result (more details on that below). The default handler will rethrow all Throw events, but the user may override it with "MainCodeHandler" option to do something more complex, such as to somehow use the cleanup code evaluation information, transform the payload before rethrowing, etc.
- The following Throw event types are distinguished:
- The cleanup code event handler, optionally specified with "CleanupEventHandler", has the following properties:
- Signature:
cleanupHandler[eventType, payload]
where eventType is one the types listed above, and payload is the caught result of cleanup section execution. - It is not supposed to generate Throw events. Any Throw events generated inside the handler code will be silently caught and stopped from propagating outside the handler.
- The default cleanup handler returns Null, i.e. is equivalent to Function[{eventType, payload}, Null].
- The main code handler is specified with "MainCodeHandler" option and applied to the main code evaluation result (obtained by catching any Throw event possibly generated by the main code).
- There is a default handler for the main code, and the user can override it either in full or partially for specific event type or types. For the partial overrides, special rule syntax can be used to provide handler(s) for specific event types. Here are some examples:
- The default handlers for the main code Throw events do rethrow them, and for exceptions also add cleanup information to the rethrown exception. Specifically, they are equivalent to the following ones:
- If the main code executes normally (has the event type "NormalExecution"), but the cleanup code generates any Throw events, then the default handler for the main code will rethrow these cleanup code Throw events. This behavior can also be overridden, just like for the other event types.
- If the default main handler encounters an unknown event type, either for the cleanup code or the main code, it will throw an exception of special types WithExceptionalCleanupError or UnknownThrowEventType, which are defined for this implementation and are registered as subtypes of the "ErrorHandlingException" type that the Exception framework uses for its internal exceptions. Like with the behavior for other event types, this one can also be changed/overridden by the user.
| Handler syntax | Action | |
| func | override entire default handler | |
| "Throw" -> func | use custom handler func for untagged Throw events | |
| "Throw"|{"Throw", _} -> func | use custom handler func for all Throw events | |
| {"Throw" -> f1, {"Throw", _} -> f2} | use different custom handlers for different event types |
| Event type | Event handler | |
| "Throw" | Function[{e,res,cl},Throw[res]] | |
| {"Throw", tag} | Function[{e,res,cl},Throw[res,tag]] | |
| "Exception" | Function[{e,res,cl},ThrowException[res,<|"CleanupInformation"cl|>]] |
This design of withExceptionalCleanup ensures that exceptions are never silenced by default. If the main code completes normally but the cleanup phase encounters a Throw event, that event is faithfully repropagated (at least, that is the default behavior). This prevents critical resource failures from being "swallowed" by a successful main result, while still allowing for explicit overrides via the "MainCodeHandler" option.
In the case of tagged or untagged Throw events generated in the main code section, the free and arbitrary nature of their payload does not allow any information to be attached to the payload before it continues to propagate, in a general and case-independent manner. But one can still use event handlers to save the cleanup code results somewhere or otherwise use those results.
This use case highlights another advantage of exceptions: their well-structured payload eliminates the need for additional state or side effects to handle cleanup information. Because cleanup data can be consistently attached to the primary propagating exception, both scattering state across the execution stack and using global variables are avoided—both of which complicate code. While avoiding such complexity is an important advantage, there is no universal way to achieve this when dealing with arbitrary Throw events in the main code.
Implementation
Below is a possible implementation of such a spec. The code tries to be self-explanatory, but has also been heavily commented for your convenience.
(* Custom exception types used by withExceptionalCleanup *)
RegisterExceptionType[WithExceptionalCleanupError, "ErrorHandlingException"];
RegisterExceptionType[UnknownThrowEventType, WithExceptionalCleanupError];
(*
** withSideEffect[code, sideEffectCode] executes <code> then <sideEffectCode>,
** and returns the result of <code>. This particular implementation uses the
** fact that Function evaluates all passed arguments in the order they were
** passed, but then discards the unused ones.
*)
withSideEffect = Function[#];
(* One way to extract the Catch/Throw tag used internally by Exceptions framework *)
$ErrorHandlingTag := $ErrorHandlingTag = Quiet @ Catch[ThrowException["Test"], _, #2&]
(*
** catchAll[expr] catches all Throw events generated inside <expr>, and
** returns an association <|"Result" -> result, "EventType" -> etype|>,
** where <etype> may be one of:
** "Exception"
** "Throw"
** {"Throw", tag}
** "NormalExecution"
*)
SetAttributes[catchAll, HoldAll]
catchAll[expr_] :=
Block[
(* One of the rare cases where speed difference really matters enough to prefer Block over Module *)
{taggedThrown = {}, untaggedThrown = True, normalExecution = False},
<|
(*
** Quiet is used here, since Catch[expr, _, func] catches all tagged Throw events,
** and that includes exceptions. But whereas ThrowException would not emit this
** message (ThrowException::uncaught) when called from within any CatchExceptions,
** it would do that from within Catch. Sticking CatchExceptions[expr, All -> Exception]
** inside the inner Catch would have been a cleaner choice, but would slow the
** function down very significantly for all Throw events other than exceptions.
*)
"Result" -> Quiet[
(*
** PreemptProtect is needed to ensure atomicity inside, for the variable
** assignments / state to not be subtly broken by preemptive evaluations.
*)
PreemptProtect @ Catch @ withSideEffect[
Catch[
withSideEffect[expr, normalExecution = True],
_,
Function @ First[taggedThrown = {##}]
],
untaggedThrown = False (* If this line is reached, there was no untagged Throw event *)
],
ThrowException::uncaught
],
"EventType" -> Which[
TrueQ[normalExecution],
"NormalExecution",
MatchQ[taggedThrown, {_, $ErrorHandlingTag}],
"Exception",
taggedThrown =!= {},
{"Throw", Last @ taggedThrown},
TrueQ @ untaggedThrown,
"Throw",
True,
"UnknownEventType"
]
|>
]
(*
** A function to rethrow some Throw event/payload. The idea is to encapsulate
** all details on rethrowing Throw events, so that it knows how to rethrow
** an event of any type, and the calling code can then be made more general.
*)
rethrow["Throw", expr_] := Throw[expr]
rethrow[{"Throw", tag_}, expr_] := Throw[expr, tag]
rethrow["Exception", e_] := ThrowException[e]
rethrow[etype_, expr_] := ThrowException[
UnknownThrowEventType,
<|"FailingFunction" -> rethrow, "EventType" -> etype, "Payload" -> expr|>
]
rethrow[args___] :=
ThrowException[
WithExceptionalCleanupError,
<|"FailingFunction" -> rethrow, "FalingFunctionArguments" -> {args}|>
]
(*
** These rules define the default behavior of main code Throw event
** handler, for various event types. The order of the rules matters,
** in particular for performance: the "NormalExecution" event is
** consiedered first, to make sure it is handled as fast as possible.
*)
$defaultMainCodeHandlerRules[result_, cleanupInfo_] := {
(* Main code executes normally *)
"NormalExecution" :> Replace[
cleanupInfo["EventType"],
{
"NormalExecution" -> result, (* Cleanup code executes normally. Return the main code result *)
event_ :> rethrow[event, cleanupInfo["RawResult"]] (* Cleanup code throws. Rethrow the cleanup code Throw event*)
}
]
,
(* Main code throws an exception. Rethrow it with attached cleanup code information *)
e:"Exception" :> rethrow[
e,
Exception[result, <|"CleanupInformation" -> cleanupInfo|>]
]
,
(* Catchall rule. It will rethrow other Throw events of the main code *)
event_ :> rethrow[event, result]
}
(*
** makeMainHandler[expr] creates Throw event handler for the main code
** path in withExceptionalCleanup[main, cleanup]. The resulting event
** handler takes 3 arguments: event type, main code result, and cleanup
** information.
**
** The <expr> can be of one of the following forms:
** (a) eventType -> handler
** (b) {eventType1 -> handler1, eventType2 -> handler2, ...}
** (c) handler
** In cases (a) and (b) event type can be a single event, such as e.g.
** "Throw" or {"Throw", someTag}. But it can also be a pattern, e.g.
** "Throw"|"Exception", or {"Throw", _}.
**
** For the rule syntax in (a) and (b), only the event types specified
** in the rules, are overridden, whereas for others, default handler /
** rules are in effect. The case (c) OTOH overrides the entire event
** handler for all event types.
**
** Default event handling rules do rethrow the caught events. However,
** when they are overridden by custom event handlers, those handlers
** are now responsible for rethrowing the relevant events they handle,
** in cases when you need them to.
*)
(* Default handler *)
makeMainHandler[{}] = makeMainHandler[Automatic] = Function[{eType, res, cinfo},
Replace[eType, $defaultMainCodeHandlerRules[res, cinfo]]
];
(* Event type-specific overrides using rule syntax *)
makeMainHandler[rule_Rule] := makeMainHandler[{rule}]
makeMainHandler[{rules__Rule}] :=
With[{fullRules = {rules, _ :> makeMainHandler[Automatic]}},
(* # is event type, #2 is result, #3 is cleanup info *)
Function[Null, Replace[#, fullRules][##]]
]
(* Generic handler, full override *)
makeMainHandler[handler_] := handler
(*
** The main function
*)
SetAttributes[withExceptionalCleanup, HoldFirst]
Options[withExceptionalCleanup] = {
"CleanupEventHandler" -> Automatic,
"MainCodeHandler" -> Automatic
};
withExceptionalCleanup[{code_, cleanup_}, opts : OptionsPattern[]] :=
Block[ (* One of the rare cases where speed difference really matters enough to prefer Block over Module *)
{ mainResult, cleanupInformation, finalResult }
,
WithCleanup[
WithCleanup[
mainResult = catchAll[code] (* Catch all Throw events in the main code *)
,
cleanupInformation = Replace[
catchAll[cleanup], (* Catch all Throw events in the cleanup code *)
KeyValuePattern[{"Result" -> res_, "EventType" -> etype_}] :> <|
"RawResult" -> res,
"EventType" -> etype,
"HandledResult" -> Replace[
(* The handler only runs when explicitly provided *)
OptionValue["CleanupEventHandler"],
{
Automatic -> Null,
handler_ :> Lookup[
(*
** The extra catchAll here is a safety measure, to ensure
** that any Throw events generated inside the handler,
** are confined and do not propagate outside (the handler
** is not supposed to generate uncaught Throws).
*)
catchAll @ handler[etype, res], (* Call cleanup code event handler *)
"Result"
]
}
]
|>
]
],
(*
** This check is needed since under some circumstances, such as e.g.
** explicit aborts generated in the main code, the variable mainResult
** may stay unassigned at this evaluation point.
*)
If[AssociationQ @ mainResult,
(*
** The main code handler may rethrow a (possibly transformed) payload
** of the caught Throw, or not. Default handler rethrows, but the custom
** user-specified one may do something else. Rule syntax allows to
** conveniently specify custom handlers per event type or types, while
** keeping default handlers for all the other event types.
*)
finalResult = makeMainHandler[OptionValue["MainCodeHandler"]] @@ Append[
Lookup[mainResult, {"EventType", "Result"}],
cleanupInformation
]
]
];
(*
** This code block is only reached if Throw events of main code are not rethrown,
** and the main and cleanup code did not generate some other interrupts, like e.g.
** explicit aborts.
*)
If[AssociationQ @ mainResult,
finalResult
,
(*
** This guards against some logical flaw in code, should not normally happen.
** The ThrowException below plays a role of an assertion.
*)
ThrowException[
WithExceptionalCleanupError,
<|
"FailingFunction" -> withExceptionalCleanup,
"FalingFunctionArguments" -> {HoldForm[code], HoldForm[cleanup], opts}
|>
]
]
]
Note that the syntax of withExceptionalCleanup is slightly different from WithCleanup: you need to put your main and cleanup code in an extra list. This was done to allow to extend it to the three-argument form in principle, and also for it to be able to have options without overcomplicating the code evaluation control.
Examples below illustrate various usage scenarios, where some Throw events may happen in main code, cleanup code, or both. What unifies all these cases is that one never gets into the double (or multiple) Throw event simultaneous propagation mode.
Simple Examples
In the following groups of examples, an exception is thrown in the main code, whereas in cleanup code, various other events happen.
withExceptionalCleanup[{ThrowException["Main"], ThrowException["Cleanup"]}]withExceptionalCleanup[{ThrowException["Main"], Throw["Cleanup"]}]withExceptionalCleanup[{ThrowException["Main"], Throw["Cleanup", $cleanup]}]withExceptionalCleanup[{ThrowException["Main"], Failure["Cleanup", <||>]}]CatchExceptions[All -> handler] @ withExceptionalCleanup[
{ThrowException["Main", 42], Throw["Cleanup", $cleanup]}]In the following groups of examples, the main code does not generate any Throw events, but the cleanup code does, and those events are then rethrown/propagated further.
withExceptionalCleanup[{Print["Main"], ThrowException["Cleanup"]}]withExceptionalCleanup[{Print["Main"], Throw["Cleanup"]}]withExceptionalCleanup[{Print["Main"], Throw["Cleanup", $cleanup]}]withExceptionalCleanup[{Print["Main"]; 42, Print["Cleanup"]}]This rethrowing behavior can be changed by overriding the main code event handler, if you prefer to do something else, such as e.g. handle cleanup code events somehow, and then simply return the main code result, or anything else you may wish to do with it.
Using Custom Event Handler for the Cleanup Code
The next group of examples covers cases where in the main code some generic Throw event happens (tagged or untagged; the untagged case is illustrated, but the same logic would apply to the tagged case as well), rather than an exception.
withExceptionalCleanup[{Throw["Main"], Throw["Cleanup"]}]withExceptionalCleanup[
{Throw["Main"], Throw["Cleanup"]}, "CleanupEventHandler" -> Function[{etype, res}, Print[etype, ", ", res] ]]withExceptionalCleanup[
{Throw["Main"], Throw["Cleanup"]}, "CleanupEventHandler" -> Function[{etype, res}, Print[etype, ", ", res]; Throw[$Failed]]
]withExceptionalCleanup[
{Throw["Main"], Throw["Cleanup"]}, "CleanupEventHandler" -> Function[Abort[]]]withExceptionalCleanup[
{Throw["Main"], Throw["Cleanup", $cleanup]}, "CleanupEventHandler" -> Function[{etype, res}, Print[etype, ", ", res] ]]withExceptionalCleanup[
{Throw["Main"], ThrowException["CleanupException", 42]}, "CleanupEventHandler" -> Function[{etype, res}, Print[etype, ", ", res] ]]withExceptionalCleanup[
{Throw["Main"], Failure["Cleanup", <||>]}, "CleanupEventHandler" -> Function[{etype, res}, Print[etype, ", ", res] ]]In this second group of examples, in order to somehow handle the Throw events happening in the cleanup code, one really has to use the handlers, because there is no general way to attach those results to the main propagating Throw event without changing its payload. And the use of these handlers is only meaningful if they produce side effects.
As noted earlier, this is one place where the Exceptions framework shows its superiority (exceptions are the only type of thrown objects for which one can attach the cleanup information to the rethrown exception in a general way), which comes from it being more structured and more constrained than generic ad hoc approaches based on Throw and Catch.
Using Custom Event Handler for the Main Code
One can also use withExceptionalCleanup in more powerful ways, both within the context of exceptions and more widely. In particular, one can override the main code handler. Examples below illustrate that.
In the case of normal execution of the main code (no Throw events), the default behavior of the main code handler is to rethrow any Throw event possibly generated in the cleanup code, because one of the design principles of withExceptionalCleanup is not to "swallow" any Throw events unless absolutely necessary. The following examples show how to override that behavior to instead always return the result of the main code evaluation, no matter whether the cleanup code generates any Throw events or not.
cleanupEventsSilencer = Function[{event, result, cleanupInfo},
Echo[cleanupInfo];result]withExceptionalCleanup[
{Print["Main"]; 42, ThrowException["Cleanup"]}, "MainCodeHandler" -> "NormalExecution" -> cleanupEventsSilencer]withExceptionalCleanup[
{Print["Main"];42, Throw["Cleanup"]},
"MainCodeHandler" -> "NormalExecution" -> cleanupEventsSilencer]withExceptionalCleanup[
{Print["Main"];42, Throw["Cleanup", $cleanup]},
"MainCodeHandler" -> "NormalExecution" -> cleanupEventsSilencer
]The next group of examples explores how one can override the behavior of withExceptionalCleanup for specific Throw events happening in the main code.
withExceptionalCleanup[
{Throw["Main"], Failure["Cleanup", <||>]},
"MainCodeHandler" -> {"Throw" -> List}]withExceptionalCleanup[
{ThrowException["Main"], Failure["Cleanup", <||>]},
"MainCodeHandler" -> {"Throw" -> List}]withExceptionalCleanup[
{ThrowException["Main"], Failure["Cleanup", <||>]},
"MainCodeHandler" -> {"Throw" | "Exception" -> List}]withExceptionalCleanup[
{ThrowException["Main"], Failure["Cleanup", <||>]},
"MainCodeHandler" -> {"Throw" -> List, "Exception" -> List}]withExceptionalCleanup[
{Throw["Main", $main ], Failure["Cleanup", <||>]},
"MainCodeHandler" -> func]Overriding the main code handler to effectively catch various Throw events without rethrowing them is possible, as this section demonstrated, but not recommended. But, for example, the need to add some information to the object being rethrown is a valid use case for such an override. The next two subsections explore such use cases.
Example: Adding Cleanup Information for Ad Hoc Failure-Based Exceptions
Consider now a typical scenario where using a custom main code handler is both justified and powerful. Many projects use Failure objects as a payload for Throw, to essentially propagate exceptions. In other words, they use Throw[Failure[…],someTag] and Catch[expr,someTag] where one would now use the Exceptions framework instead (which was not available in the earlier versions of the Wolfram Language).
There is no simple way to add the cleanup information to the Failure object propagating on the main code path, when one uses standard tools (WithCleanup)—one would have to add custom code inside WithCleanup, doing something similar to what withExceptionalCleanup does internally. However, this is quite easy to do with withExceptionalCleanup.
WithCleanup[Throw[Failure["SomeError", <|"Data" -> 42|>], $tag], Throw[Failure["SomeOtherError", <|"Data" -> 404|>], $tag]]$rethrowHandler[tag_] := Function[{eventType, result, cleanupInfo},
Throw[
Replace[
result,
Failure[t_, data_Association ? AssociationQ] :> Failure[t, Append[data, "CleanupInformation" -> cleanupInfo]]],
tag
]
]withExceptionalCleanup[
{Throw[Failure["SomeError", <|"Data" -> 42|>], $tag], Throw[Failure["SomeOtherError", <|"Data" -> 404|>], $tag]},
"MainCodeHandler" -> {"Throw", $tag} -> $rethrowHandler[$tag]]
Example: Resource Management for Hybrid Exception Handling Workflows
You can also use withExceptionalCleanup if you need resource management but for some reason are forced to use mixed workflows, where, for example, part of your code still uses the older approach that throws and catches Failure objects using custom tags, but some other part of your code uses the Exceptions framework.
withExceptionalCleanup[
{Throw[Failure["SomeError", <|"Data" -> 42|>], $tag],
ThrowException["SomeOtherError", <|"Data" -> 404|>]},
"MainCodeHandler" -> {"Throw", $tag} -> $rethrowHandler[$tag]]
withExceptionalCleanup[
{ThrowException["SomeError", <|"Data" -> 42|>],
Throw[Failure["SomeOtherError", <|"Data" -> 404|>], $tag]},
"MainCodeHandler" -> {"Throw", $tag} -> $rethrowHandler[$tag]]SetAttributes[withCustomCleanup, HoldAll];
withCustomCleanup[code_, cleanup_] := withExceptionalCleanup[{code, cleanup}, "MainCodeHandler" -> {"Throw", $tag} -> $rethrowHandler[$tag]];
customCatch = Function[code, Catch[code, $tag], HoldFirst];
customThrow = Function[expr, Throw[expr, $tag]];CatchExceptions[All -> handler] @ customCatch @ withCustomCleanup[
ThrowException["SomeError", <|"Data" -> 42|>],
Throw[Failure["SomeOtherError", <|"Data" -> 404|>], $tag]
]CatchExceptions[All -> handler] @ customCatch @ withCustomCleanup[
Throw[Failure["SomeError", <|"Data" -> 42|>], $tag],
ThrowException["SomeOtherError", <|"Data" -> 404|>]
]Interaction with Aborts
The following examples show how the function interacts with aborts. Basically, it just passes them through, and they are not handled in any special way by withExceptionalCleanup. The reason for that is that for aborts happening in both the main and cleanup code sections, WithCleanup does not create the "multiple abort" mode, unlike with Throw events.
withExceptionalCleanup[{Print["Main"];42, Abort[]}]withExceptionalCleanup[{Print["Main"];42, TimeConstrained[Pause[3], 2]}]withExceptionalCleanup[{Abort[];42, Print["Cleanup!"]}]withExceptionalCleanup[{Print["Main"];Abort[];42, Print["Cleanup!"];Abort[]}]func @ withExceptionalCleanup[{Print["Main"];TimeConstrained[Pause[3], 2];42, Print["Cleanup!"]}]func @ withExceptionalCleanup[{Print["Main"];TimeConstrained[Pause[3], 2];42, Print["Cleanup!"];Abort[]}]withExceptionalCleanup[{Print["Main"];TimeConstrained[Pause[3], 2];42, ThrowException["Cleanup"]}]withExceptionalCleanup[{ThrowException["Main"], Print["Cleanup!"];Abort[]}]Note that in all these cases, the behavior of withExceptionalCleanup is essentially the same as that of WithCleanup itself. You can always wrap your main and/or cleanup code in explicit CheckAbort, if you need to change it.
Performance
The implementation of withExceptionalCleanup tries to be as simple and fast as possible. However, there still is some overhead involved in using it, as compared to using just WithCleanup. The most important code path to consider here is the one of normal execution on both main and cleanup branches, because this is what happens most of the time and should therefore be as fast as possible.
WithCleanup[1, 2]//RepeatedTimingwithExceptionalCleanup[{1, 2}]//RepeatedTimingIt is still hoped that an overhead of a few dozen microseconds is acceptable in most scenarios where one may want to use WithCleanup. For those cases where it is not, you will likely have to resort to completely custom solutions, perhaps reusing some withExceptionalCleanup implementation patterns, but replacing generic parts (handlers, event system) with some very specific code, relevant only for your particular situation or setup.
Design and Implementation Notes
The withExceptionalCleanup function is specifically designed to preclude the multiple-propagation mode that the native WithCleanup creates. If both the main and cleanup blocks generate Throw events, WithCleanup forces them into a collision where one inevitably suppresses the other, leading to a silent and unrecoverable loss of error context. This mode also leads to the multiple (and generally unspecified number of) Catch/CatchExceptions calls required to fully catch all propagating events—the problem that was discussed at the start of this section.
By contrast, withExceptionalCleanup is collision-averse by construction. It intercepts these sequential events before they can collide and reconciles them through a deterministic, hierarchical dispatcher. By moving from event competition to event coordination, this approach ensures that both events can be properly handled, and only a single event (or none at all, if no events were generated in either code section) propagates further, when the evaluation leaves withExceptionalCleanup.
By using a dispatch system based on event types, the "option bloat" that typically plagues complex control-flow wrappers was avoided. Rather than managing a messy collection of Boolean flags or specialized options, this design uses pattern matching on a single event type. This results in a much cleaner design, where the implementation remains robust even as new exception types or edge cases are addressed.
Because makeMainHandler utilizes Replace on the "EventType", the system is open for extension without modification to the core engine. Users can match against complex patterns—such as {"Throw", _Integer} to catch only numeric tags—without the framework needing to understand the semantics of those tags.
As mentioned in the code comments, the catchAll dispatcher intentionally uses a low-level Catch with a blank pattern (_) for all tagged Throw events including exceptions, rather than the higher-level CatchExceptions. While the latter provides more "semantic" filtering, its overhead is significant in tight loops. By using a raw Catch and manually disambiguating the "EventType" in the Which block, the propagation speeds are made closer to native ones for standard Throw events.
Regarding the interaction with aborts, the design of withExceptionalCleanup has been intentionally made as unopinionated as possible, and it follows directly the design of WithCleanup itself. The reason for that choice is that for aborts happening in both the main and cleanup code sections, WithCleanup does not create the "multiple abort" mode, unlike with Throw events. Also, one can always change the current behavior of withExceptionalCleanup regarding aborts by wrapping the main and/or cleanup code in CheckAbort explicitly. On the other hand, including aborts in the design/implementation as another event type alongside Throw events, while possible, would degrade the performance without bringing in a lot of extra value.
The implementation uses two custom exception types: WithExceptionalCleanupError and its child type UnknownThrowEventType. Both are descendants of "ErrorHandlingException", which is the type used by the Exceptions framework for internal exceptions. The use of these exception types guarantees that withExceptionalCleanup will report some internal errors, rather than swallow them.
One might argue that a single handler could manage both main and cleanup events (and, in fact, the main code handler can). However, those were intentionally decoupled to simplify the user experience. Forcing a single handler to manage both side effects and the complex rethrowing logic of withExceptionalCleanup would be unnecessarily error prone. The current design ensures that the main handler—and its critical propagation logic—only needs to be overridden when absolutely necessary. In short, the cleanup handler manages side effects and observation, while the main handler manages control flow and propagation.
Usage and Customization
Whereas the main code handler can be overridden to effectively catch various Throw events (including exceptions) happening in the main code and stop their propagation entirely (rather than rethrowing them), this handler is intended to be more of a specialized tool, and it is not a good style to use it to replace proper Catch or CatchExceptions calls. The general logic that the default handler for the main code has is to rethrow all such caught events. Overriding it would make sense if you want, for example, to add cleanup information to the main propagating object somehow, but then to still rethrow it—the case that was illustrated by the previous subsections. The other valid use case for the main code handler override is when you want to stop Throw events generated by cleanup code from propagation, even when the main code executes normally (for example, you may want to return a Failure object instead that would include the information on caught events in the cleanup code).
In terms of the code ergonomics, it may become less convenient to use withExceptionalCleanup with options, particularly for larger code blocks in the main and/or cleanup code, and the resulting code may become harder to read. One way to deal with this problem is by defining a shortcut function, as was done in one of the previous examples—which defined a custom withCustomCleanup function:
SetAttributes[withCustomCleanup, HoldAll];
withCustomCleanup[code_, cleanup_] := withExceptionalCleanup[{code, cleanup}, "MainCodeHandler" -> {"Throw", $tag} -> $rethrowHandler[$tag]];Summary
This subsection presented the design and implementation of a custom version of WithCleanup that is more constrained and opinionated but guarantees predictable handling and propagation of Throw events in a single-event propagation mode and completely excludes the error-prone and unpredictable multiple-event propagation mode. The built-in event handlers for both the main and cleanup code paths make this function both powerful and configurable, despite its more constrained nature. It may be applicable to a significantly wider set of use cases than just those involving only the isolated uses of the Exceptions framework.
Summary and Conclusions
Due to the double/multiple exceptions problem, one needs extra care when using WithCleanup with exceptions (as well as with Throw and Catch in general). You have two options:
- "Manually" ensure that the main and cleanup code inside WithCleanup can never both throw exceptions simultaneously.
- Replace the use of bare WithCleanup with some custom solution, which solves this problem for you.
The "manual" bulletproofing of your uses of WithCleanup may work fine for simpler use cases and smaller projects, but may become progressively prone to errors in larger projects and more complex scenarios. Therefore, it is recommended to use some form of the WithCleanup replacement, such as withExceptionalCleanup (you can probably implement your own versions along similar lines, if it does not fit your needs), rather than bare WithCleanup calls, both when using it together with the Exceptions framework, and in general when you propagate some information using Throw and Catch.
In general, the complexity of this situation clearly calls for a built-in/system solution that would make some design choices for you and simplify the logic, taking as much of it as possible off your shoulders. This has not been done yet on the system/framework level, which is one reason why this section exists. Hopefully, such a solution will appear in the future versions of the Exceptions framework.
Exceptions vs. Throw and Catch
General Observations
Throw and Catch represent the core Wolfram Language primitives to implement nonlocal control flow. The Exceptions framework described here is implemented on top of them, as are the error-handling primitives Enclose and the Confirm family of functions.
Whereas there is nothing wrong with using Throw and Catch directly to obtain exception-like behavior, there are several useful features, which exceptions provide, that Throw and Catch do not offer directly, and that may particularly benefit larger projects.
Throw and Catch allow one to throw and catch absolutely anything, whereas exceptions provide a more structured, more constrained and arguably more expressive way to organize nonlocal control flow in your code.
The table below summarizes some of the differences:
| Feature | Throw/Catch | exceptions | |
| Payload structure | arbitrary | association, some keys reserved | |
| Error's identity | tag, external to error | type, contained within Exception object | |
| Exception types | anything | strings or symbols | |
| Untagged exceptions | alowed | disallowed, this is a hard error | |
| Type inheritance | no built-in way, patterns | built-in for registered types | |
| Handler dispatch | needs manual code | built-in | |
| Speed | very fast | significantly slower |
When to Use Exceptions
Exceptions are a more specialized tool than Throw and Catch, and are intended to be used for structured error handling in larger projects. Compared to Throw and Catch, exceptions offer the following advantages for such use cases:
- Exception types and type hierarchies allow you to organize both the error types and the ways those errors are propagated within a given project or its parts. For example, some exception types may be local to certain parts of the project (such as subpackages), whereas others may be used to propagate errors across those parts.
- The identity of the propagated errors. With Catch and Throw, the payload itself does not generally carry any information about the type or identity of error being propagated. In contrast, an Exception object is always self-contained, carrying both exception type(s) and the data payload. Which, for example, makes it easy to rethrow such exceptions or convert them to Failure objects.
- Consistency in exception handling. By restricting the shape of error data that is propagated to associations, exceptions provide a more clear protocol for handling such errors. Exception handlers are always passed this association with the error data, and can expect that.
- Built-in selective exception catching and exception handler dispatch. This is something that one can certainly build manually with Throw and Catch, but making such implementation robust and general enough will take time and effort.
- Error-handling workflow standardization. This is important, since it is far easier to understand the code of some other project if it uses the same error-handling tools, than if every project implements its own error-handling framework.
When to Use Throw and Catch
Creating an exception is a rather computationally expensive operation as compared to, e.g. creating a Failure object, which is completely inert.
RegisterExceptionType[abcException];
Exception[abcException, 42]//RepeatedTimingException[unregisteredTag, 42]//RepeatedTimingWhereas it is expected that the performance of the framework will be significantly optimized in future versions, it will never be able to compete with direct use of Throw and Catch.
Catch[Throw[42, unregisteredTag], unregisteredTag]//RepeatedTimingThe reason one can get away with relatively slow exception handling is that most of the time the code works without errors, and when the error is encountered, it is often less critical (to a certain extent, of course) how fast it is propagated.
Still, there can be cases when the overhead of exceptions will be unacceptable, in which case Throw and Catch offer a much faster, if a simpler, alternative. In particular, not all cases when one wants nonlocal transfer of control represent errors. Sometimes it simply makes sense to use it as a form of nonlocal Return. In such cases, Throw and Catch are almost always the better tool.
Differences between Exceptions and Other Types of Failures
$Failed and Failure
The fundamental difference of these values with an Exception object is that the latter is intended to be thrown, whereas the former are typically returned from functions.
What this means is that whereas it is very typical to have error handling code that may look like this:
If[FailureQ[expr], Return[expr], someFunction[expr]]It will be a lot less common to see something of this kind:
If[ExceptionQ[expr], Return[expr], someFunction[expr]]Simply because the only sensible use case when an Exception object is returned by some function is for it to be thrown, and on the other hand, exception handlers only get passed an association of exception data, rather than the caught Exception object itself.
It is not uncommon, however, to see code where Failure objects or even $Failed are used as a payload for Throw, thereby propagating these values as exceptions.
Throw[Failure["SomeError", <|"Data" -> 42|>], "SomeTag"]Throw[$Failed, "SomeTag"]In such cases, it is almost always better to use an Exception object instead, since it has been specifically designed for this type of error propagation and has a number of advantages over bare Throw and Catch.
ThrowException @ Exception["SomeTag", Failure["SomeError", <|"Data" -> 42|>]]ThrowException @ Exception["SomeTag", <|"Data" -> 42, "ExceptionFailureTag" -> "SomeError"|>]%["ExceptionFailure"]TerminatedEvaluation
TerminatedEvaluation[…] represents a result of the computation, within which an event somewhat similar to throwing an exception has occurred:
ClearAll[res, x]
res = x = x + 1However, the distinctive difference w.r.t. Exception objects is that TerminatedEvaluation[…] is a normal value that is being returned:
resSo, whereas this value does represent the result of internal breaking out of some code, the result itself is a normally returned expression, which can be further manipulated. On the contrary, an Exception object, once thrown, propagates to the top level and cannot be directly used in the surrounding code as a return value.
func[x = x + 1]func[ThrowException["Tag"]]otherFunc[CatchExceptions["Tag"]@func[ThrowException["Tag"]]]$Aborted
$Aborted is somewhat similar to an Exception object, in that under some circumstances (e.g. explicit call to Abort[] or keyboard-based aborts), this value is propagated to the top level, breaking out of all enclosing functions:
aborted = {1, 2, Abort[], 4, 5}Also, $Aborted can in some cases be returned and, e.g. assigned to some variable.
aborted = MemoryConstrained[Range[10 ^ 6], 10000]aborted === $AbortedExceptions, just like $Aborted, can either be thrown:
res = {1, 2, ThrowException["Error"], 4, 5}
resexc = Exception["Error"]
excHowever, the propagation mechanisms are different: for exceptions, the mechanism is based on the underlying Throw and Catch, whereas Abort uses its own propagation mechanism.
One can, if one wishes, convert Abort instances to exceptions, with the code similar to this:
SetAttributes[abortToException, HoldFirst]
abortToException[expr_] := With[{exc := ThrowException["AbortGenerated", HoldForm @ expr]},
Replace[CheckAbort[expr, exc], $Aborted :> exc]
]So that in the following example inputs Abort instances, generated inside the code, are converted to exceptions.
abortToException[MemoryConstrained[Range[10 ^ 6], 10000]]abortToException[{1, 2, Abort[], 4, 5}]Confirm/Enclose Interoperability
The error-handling primitives Enclose and the Confirm family of functions represent an approach to the error handling that is complementary to exceptions, but both approaches use the same core principles.
The approach based on Confirm and Enclose has two distinct flavors:
- Lexical (or tagless). In this approach, Enclose must be lexically present in the same piece of code that contains the calls to the Confirm family of functions inside. In this case, the Confirm family of functions inside such code can be viewed as custom and more convenient forms of Return, but not as propagating exceptions, because their action does not lead to nonlocal control flow outside of the function where they are used.
- Dynamic, and using explicit tags. In this form, these primitives do realize true exceptions. Enclose[expr,func,tag] plays a role very similar to CatchExceptions[expr,tag] (and in particular, expr does not have to contain calls to the Confirm family of functions directly; they can be called somewhere deeper in the execution stack, just like for exceptions), and the Confirm family of functions can be viewed as custom versions of ThrowException.
When the interoperability of the Confirm/Enclose method and exceptions is discussed, what is meant is the second, dynamic version of the former.
For the purposes of this section, the following two symbolic exception types will be registered, the second being the child type of the first:
RegisterExceptionType[confirmException]
RegisterExceptionType[confirmSubException, confirmException]Comparison of the Approaches
Both approaches implement exception propagation. There are two natural questions then:
This table compares various aspects of the two approaches:
| Feature description | Confirm/Enclose | ThrowException/CatchExceptions | |
| Main purpose | converting errors in called functions to exceptions | general nonlocal control flow based on exception objects | |
| Converting errors in called functions to exceptions | very convenient | more verbose, clutters code with If statements and the like | |
| General exception throwing | not very natural | natural, built for this | |
| Selective exception catching | moderately convenient, based on patterns | very convenient: built-in type hiearchies and exception-handling dispatch based on types | |
| Usability of caught result | medium, Enclose always returns the same type of Failure (although can be customized by the second parameter to Enclose) | high, one defines completely custom exception handlers | |
| Propagating object's type | special type of Failure object | Exception object | |
| Propagating failure | not self-contained, the tag is provided separately | self-contained, Exception object contains its type(s) and its full identity |
An important thing to emphasize is that the two systems do not compete with each other but complement each other. The Exceptions framework has been designed to interoperate with Confirm and Enclose. This means that you can not only pick the approach most useful for a particular use case but also use them in combinations.
How the Interoperability Works
The design of the two subsystems is different enough that they cannot be made easily interoperable in the full set of their intended use cases. However, there is a very practically important subset of the use cases, where they can be made 100% compatible with each other. These are cases when one uses registered symbolic exception types to create exceptions—be those generated by the Confirm family of functions or by ThrowException.
Here is a partial list of interoperability features for such use cases:
- Confirm family of functions and Enclose use ThrowException/CatchExceptions internally, when used with registered symbolic exception types.
- Enclose[expr,func,registeredType] will catch all Confirm, ConfirmBy, etc. generated exceptions, with tag being registeredType (this worked before) or any of its child types (this is a new feature brought by the Exceptions framework). It will also catch any exception thrown by ThrowException, under the same condition on types.
- CatchExceptions[expr,registeredTypehandler] will catch all exceptions generated by Confirm, ConfirmBy, etc. that use registeredType (or any of its child types).
For all other types of tags, the Exceptions framework and Confirm/Enclose operate completely independently. This design choice, in particular, guarantees that all existing code using Confirm/Enclose with explicit tags continues to work exactly as before.
Using Type Inheritance in Confirm/Enclose Workflows
Using registered exception types can benefit your Enclose/Confirm workflows, even if you do not ever plan to mix them with exceptions or use exceptions explicitly.
The following examples illustrate how Confirm and Enclose can use the type inheritance of registered exception types.
Enclose[
func[Confirm[$Failed, <|"SomeKey" -> "SomeValue"|>, someTag]],
Identity,
someTag
]Enclose[
func[Confirm[$Failed, <|"SomeKey" -> "SomeValue"|>, confirmException]],
Identity,
confirmException
]Enclose[
func[Confirm[$Failed, <|"SomeKey" -> "SomeValue"|>, confirmSubException]],
Identity,
confirmException
]The same will work on all other functions of the Confirm family.
Enclose[
func[ConfirmBy[1, EvenQ, <|"SomeKey" -> "SomeValue"|>, confirmSubException]],
Identity,
confirmException
]Catching Thrown Exception Objects with Enclose
Despite their syntactic differences, Enclose used with an explicit tag is a lot like CatchExceptions. For the registered exception types, this analogy becomes most direct.
The example below shows how Enclose can directly catch exceptions generated by ThrowException.
Enclose[
func[ThrowException[confirmException, <|"SomeKey" -> "SomeValue"|>]],
Identity,
confirmException
]As before, the exception type inheritance also works.
Enclose[
func[ThrowException[confirmSubException, <|"SomeKey" -> "SomeValue"|>]],
Identity,
confirmException
]Catching Exceptions Generated by the Confirm Family of Functions with CatchExceptions
The analogy between Enclose and CatchExceptions, explored in the previous section, also works in the other direction: CatchExceptions can catch all exceptions generated by the Confirm family of functions, as long as they use one of the registered exception types.
CatchExceptions[confirmException] @ func[
Confirm[
$Failed,
<|"SomeKey" -> "SomeValue"|>,
confirmException]
]CatchExceptions[confirmException] @ func[
Confirm[
$Failed,
<|"SomeKey" -> "SomeValue"|>,
confirmSubException]
]When CatchExceptions uses explicit exception handler(s), the exception data passed into the handler(s) contains the "flattened" contents of the confirmation Failure object, rather than that object itself.
CatchExceptions[confirmException -> handler] @ func[
Confirm[
$Failed,
<|"SomeKey" -> "SomeValue"|>,
confirmException]
]CatchExceptions[confirmException -> (#Information["SomeKey"]&)] @ func[
Confirm[
$Failed,
<|"SomeKey" -> "SomeValue"|>,
confirmException]
]You can use built-in exception-handling dispatch to your advantage in such mixed workflows.
exceptionHandler = CatchExceptions[{
confirmSubException -> Function[ Print["SubError"]; #Information],
confirmException -> Function[ Print["Main error"]; #Information]
}];exceptionHandler @ func[
Confirm[
$Failed,
<|"SomeKey" -> "SomeValue"|>,
confirmSubException]
]exceptionHandler @ func[
Confirm[
$Failed,
<|"SomeKey" -> "SomeValue"|>,
confirmException]
]Performance and Optimizations
The main purpose of the Exceptions framework is to help organize error handling, propagation and reporting in large projects. The current version of the framework has been implemented entirely in the top-level Wolfram Language code. There is a certain performance penalty to pay for using Exception objects, and it may be not small.
The reason why such a performance penalty can still be acceptable in many or even most practical use cases is that error propagation code paths usually have more relaxed performance requirements, simply because encountering errors is more an exceptional case, happening much less frequently than the normal code execution.
However, certain use cases with very tight performance requirements may make the overhead incurred by using the Exceptions framework unacceptable. The sections that follow should help you navigate the performance landscape of the Exceptions framework and get a better idea about its applicability to your particular use case.
For the purposes of this section, the following exception types will be registered:
RegisterExceptionType[abcException]
RegisterExceptionType[abcSubException, abcException]Performance Limitations of Exceptions Framework
The table below summarizes typical performance characteristics of the core functions in the current version of the framework (which are also machine- and architecture-dependent):
| Function / usage pattern | Typical overhead, milliseconds | |
| Exception["StringType", data] Exception[unregisteredSymbol, data] | 0.1 - 0.2 | |
| 1 - 3 | ||
| Exception[<|data|>] | 0.05 | |
| RegisterExceptionType[type] | 1 | |
| Exception[...]["ExceptionData"] | 0.05 - 0.1 | |
| Exception[...]["prop"] | 0.01 - 0.02 | |
| ExceptionQ[Exception[...]] | 0.0005 - 0.001 | |
| CatchExceptions[42, All] | 0.005 - 0.01 |
Whereas it is expected that the framework will be better optimized in future versions, and also there are some workarounds and manual optimizations one can employ (which are discussed below), there may be cases where exceptions would be fundamentally the wrong tool for the job.
As one such example, you should avoid using the CatchExceptions/ThrowException combination inside otherwise lightweight functions that are called in a tight loop. If you need some nonlocal flow of control there, perhaps Catch and Throw would be more appropriate.
Here are some categories of use cases where exceptions should excel and be most appropriate to use:
- You have a large enough code base that a typical evaluation goes through several intermediate functions.
- You use exceptions to propagate errors (which represent comparatively rare behavior and not the default execution path), rather than just for the nonlocal flow transfer that happens every time the code runs.
- Many of the propagated errors are either unrecoverable (and thus reported to the end user by your top-level functions) or otherwise such that the requirements on error propagation time are not as strict as those for the normal code execution (which is most often the case).
In the sections that follow, the performance characteristics summarized here are considered in more detail. Also, various optimizations and workarounds are discussed, which can be used to improve the performance.
Exception Object Constructor
The Exception constructor may currently be one of the most expensive parts of the Exceptions framework, particularly for the registered exception types.
Using Main Exception Constructor
Examples below will illustrate the typical cost of calling the standard Exception constructor.
Exception[abcSubException]//RepeatedTimingException[SomeSymbolicException]//RepeatedTimingException["SomeError"]//RepeatedTimingIt is expected that in the future versions of the framework, the Exception constructor will be significantly optimized, to reduce this overhead.
However, there are a few things one can do to improve matters even now, if the performance requirements are tight.
Using Copy Constructor
The Exception copy constructor can be significantly faster, since it starts with an already constructed exception.
exc = Exception[abcSubException]Exception[exc, <|"Data" -> 42|>]//RepeatedTimingOne thing you can do is to create some custom function that would create and memoize exceptions without any data and then reuse those.
exceptionMemo[type_] := exceptionMemo[type] = Exception[type]Exception[exceptionMemo[abcSubException], <|"Data" -> 42|>]//RepeatedTimingCurrently, such optimizations require this type of manual setup. In the future, some of these types of optimizations, along with others, are likely to be incorporated in the Exceptions framework.
Using Low-Level Constructor
Another thing you can do, which is faster still, is to use the low-level Exception constructor (see Constructing Exception Objects for more details). This, however, requires more care, particularly for exception type expansion, in cases when the exception type has parent types.
Exception[<|"ExceptionTag" -> abcSubException, "ExceptionTagList" -> {abcSubException, abcException}, "Data" -> 42|>]//RepeatedTimingException[<|"ExceptionTag" -> abcSubException, "ExceptionTagList" -> {abcSubException}, "Data" -> 42|>]//RepeatedTimingCatchExceptions[abcException] @ ThrowException[<|"ExceptionTag" -> abcSubException, "ExceptionTagList" -> {abcSubException}, "Data" -> 42|>]Exception[<|"ExceptionTag" -> abcSubException, "Data" -> 42|>]//RepeatedTimingexceptionData[type_, data_Association] := Join[data, exceptionData[type]];
exceptionData[type_] := exceptionData[type] = KeyTake[Exception[type]["ExceptionData"], {"ExceptionTagList", "ExceptionTag"}]Exception[exceptionData[abcSubException, <|"Data" -> 42|>]]//RepeatedTimingThe same comment goes here as for the previous subsection: such optimizations, among others, are likely to be incorporated in the future versions of the Exceptions framework.
One reason this has not been done already is that types can be unregistered and re-registered, and that would invalidate the naive caching suggested here (which is something you may want to also keep in mind if following these optimization suggestions), so more care is needed to incorporate such caching but ensure correctness in all cases.
Exception Properties Extraction
For the purposes of this section, some sample Exception object is stored in a variable and its Failure object counterpart in another variable:
exc2 = Exception[abcSubException, <|"Data" -> 42|>]
failure = exc2["ExceptionFailure"]The following examples illustrate the overhead of extracting various properties.
exc2["ExceptionData"]//RepeatedTimingexc2["ExceptionFailure"]//RepeatedTimingexc2["ExceptionTag"]//RepeatedTiming
exc2["ExceptionTagList"]//RepeatedTiming
exc2["Data"]//RepeatedTimingfailure["ExceptionTagList"]//RepeatedTiming
failure["Data"]//RepeatedTimingThere is a good chance that property extraction will be at least somewhat faster in the future versions of the framework.
CatchExceptions and ThrowExceptions
The implementation of CatchExceptions tries to do as little as possible in case of normal code execution (no exception thrown). Still, you may expect 5–10 microseconds overhead (this number is machine- and architecture-dependent) just from CatchExceptions, added to the execution of your code:
CatchExceptions[42, All]//RepeatedTimingThe current overhead of ThrowException is much more significant, and is of the order of 100–200 microseconds.
Block[{Message}, Catch[ThrowException[exc2], _]]//RepeatedTimingCatch[Throw[exc2, $tag], _]//RepeatedTimingCatch[ErrorHandling`Exceptions`Internal`ThrowAny[exc2], _]//RepeatedTimingCatchExceptions[All -> handler] @ ErrorHandling`Exceptions`Internal`ThrowAny[exc2]Exception Types Registration
RegisterExceptionType is another potentially costly operation. The important difference w.r.t. Exception constructor and other operations related to Exception object is that RegisterExceptionType is typically called at definition time, i.e. at the time when your package is loaded, rather than at runtime. If you register hundreds of exception types, the cumulative overhead of RegisterExceptionType may become significant.
RegisterExceptionType[abcException]//RepeatedTimingOne thing you can do to somewhat remedy this situation is to delay the type registration until runtime (the first use of the type), in a fashion similar to how symbols autoloading works.
ClearAll[setDelayedTypeRegistration]
SetAttributes[setDelayedTypeRegistration, HoldAll]
setDelayedTypeRegistration[type_Symbol, parents_List : {}] := With[{protected = Unprotect[type]},
ClearAll[type];
type := (
Unprotect[type];
Unset[type]; (* This destroys the original definition for <type> *)
RegisterExceptionType[type, parents];
type
);
Protect[protected]];
setDelayedTypeRegistration[type_Symbol, parent : _Symbol | _String] := setDelayedTypeRegistration[type, {parent}];setDelayedTypeRegistration[childExceptionType, {parentExceptionType}]//RepeatedTimingThrowException[childExceptionType, <|"Data" -> 42|>]Like for several other functions, it is likely that eventually this type of optimization will be incorporated into the framework itself. But in the meantime, this is a viable option to speed up the loading of your package, particularly if you have a large number of error types to register.
You can also use a hybrid approach, eagerly registering some of your exception types and using lazy/delayed type registration for the other ones.