The predicates catch/3 and throw/1 provide ISO compliant raising and catching of exceptions.
The overhead of calling a goal through catch/3
is comparable to
call/1.
Recovery from an exception is much slower, especially if the exception
term is large due to the copying thereof or is decorated with a stack
trace using, e.g., the library library(prolog_stack)
based
on the
prolog_exception_hook/4
hook predicate to rewrite exceptions.
ISO demands that throw/1 make a copy of Exception, walk up the stack to a catch/3 call, backtrack and try to unify the copy of Exception with Catcher. SWI-Prolog delays backtracking until it actually finds a matching catch/3 goal. The advantage is that we can start the debugger at the first possible location while preserving the entire exception context if there is no matching catch/3 goal. This approach can lead to different behaviour if Goal and Catcher of catch/3 call shared variables. We assume this to be highly unlikely and could not think of a scenario where this is useful.80I'd like to acknowledge Bart Demoen for his clarifications on these matters.
In addition to explicit calls to throw/1, many built-in predicates throw exceptions directly from C. If the Exception term cannot be copied due to lack of stack space, the following actions are tried in order:
error(Formal,
ImplementationDefined)
, try to raise the exception without the ImplementationDefined
part.
error(resource_error(stack)
, global)
.
If an exception is raised in a call-back from C (see chapter 12) and not caught in the same call-back, PL_next_solution() fails and the exception context can be retrieved using PL_exception().
library(prolog_stack)
is loaded and an
exception of the shape error(Format, Context)
is raised
Context is extended with a backtrace. To catch an error and
print its message including a backtrace, use the following template:
:- use_module(library(prolog_stack)). ..., catch_with_backtrace(Goal, Error, print_message(error, Error)), ...,
This is good practice for a catch-all wrapper around an
application. See also main/0
from library library(main)
.
Under some conditions an exception may be raised as a result of handling another exception. Below are some of the scenarios:
'$aborted'
, also raised by abort/0.
As no stack space is required for processing this atomic exception, this
should always succeed.
If the most urgent exceptions needs to be preserved, the following exception ordering is respected, preserving the topmost matching error.
'$aborted'
(abort/0)
time_limit_exceeded
(call_with_time_limit/2)
error(resource_error(Resource)
, Context)
error(Formal, Context)
Note The above resolution is not described in the ISO
standard. This is not needed either because ISO does not specify
setup_call_cleanup/3
and does not deal with environment management issues such as (debugger)
callbacks. Neither does it define abort/0
or timeout handling. Notably abort/0
and timeout are non-logical control structures. They are implemented on
top of exceptions as they need to unwind the stack, destroy choice
points and call cleanup handlers in the same way. However, the pending
exception should not be replaced by another one before the intended
handler is reached. The abort exception cannot be caught, something
which is achieved by wrapping the cleanup handler of catch/3
into
call_cleanup(Handler, abort)
.
Before the introduction of exceptions in SWI-Prolog a runtime error was handled by printing an error message, after which the predicate failed. If the Prolog flag debug_on_error was in effect (default), the tracer was switched on. The combination of the error message and trace information is generally sufficient to locate the error.
With exception handling, things are different. A programmer may wish to trap an exception using catch/3 to avoid it reaching the user. If the exception is not handled by user code, the interactive top level will trap it to prevent termination.
If we do not take special precautions, the context information associated with an unexpected exception (i.e., a programming error) is lost. Therefore, if an exception is raised which is not caught using catch/3 and the top level is running, the error will be printed, and the system will enter trace mode.
If the system is in a non-interactive call-back from foreign code and there is no catch/3 active in the current context, it cannot determine whether or not the exception will be caught by the external routine calling Prolog. It will then base its behaviour on the Prolog flag debug_on_error:
While looking for the context in which an exception takes place, it
is advised to switch on debug mode using the predicate debug/0.
The hook
prolog_exception_hook/4
can be used to add more debugging facilities to exceptions. An example
is the library library(http/http_error)
, generating a full
stack trace on errors in the HTTP server library.
The predicate throw/1
takes a single argument, the exception term, and the ISO
standard stipulates that the exception term be of the form error(Formal,
Context)
with:
Thus, constructing an error term and throwing it might take this form (although you would not use the illustrative explicit naming given here; instead composing the exception term directly in a one-liner):
Exception = error(Formal, Context), Context = ... some local convention ..., Formal = type_error(ValidType, Culprit), % for "type error" for example ValidType = integer, % valid atoms are listed in the ISO standard Culprit = ... some value ..., throw(Exception)
Note that the ISO standard formal term expresses what should be the case or what is the expected correct state, and not what is the problem. For example:
instantiation_error
: The
problem is not that there is an unwanted instantiation, but that the
correct state is the one with an instantiated variable.
uninstantiation_error(Culprit)
: The problem is not that
there is lack of instantiation, but that the correct state is the one
which
Culprit (or one of its subterms) is more uninstantiated than
is the case.
type_error(compound,[])
. The problem is
not that []
is (erroneously) a compound term,
but that a compound term is expected and []
does not belong to that class.
User predicates are free to choose the structure of their exception terms (i.e., they can define their own conventions) but should adhere to the ISO standard if possible, in particular for libraries.
Notably, exceptions of the shape error(Formal,Context)
are recognised by the development tools and therefore expressing
unexpected situations using these exceptions improves the debugging
experience.
In SWI-Prolog, the second argument of the exception term, i.e., the
Context argument, is generally of the form
context(Location, Message)
, where:
library(prolog_stack)
library. This library recognises
uncaught errors or errors caught by catch_with_backtrace/3
and fills the
Location argument with a backtrace.
ISO standard exceptions can be thrown via the predicates exported
from library(error)
. Termwise, these predicates look
exactly like the
Formal of the ISO standard error term they throw: