Structured error handling: the dictionaries and exception_callbacks modules

Futile provides a single, structured error-handling API. In ordinary client code the public routines are reached by writing:

use dictionaries

which re-exports the raising, inspection, try/catch and query routines together with the dictionary types they operate on. The callback and severe-error policy routines are public through the companion module exception_callbacks; when they are needed the explicit use exception_callbacks line can be added. The two modules are documented separately below only to keep the source locations visible, but they describe the same error pipeline and are meant to be used together.

Raising and inspecting errors from dictionaries

The routines below are implemented in the error_handling.f90 include file and made public through dictionaries. Client code obtains them with use dictionaries; a separate use error_handling is never needed.

Description

High-level dictionary API used throughout Futile and BigDFT. A dictionary is a pointer-based tree that can represent scalars, ordered mappings, and lists. It maps directly to YAML data and is manipulated with constructors such as dict_new and list_new, the // navigation operator, and set/add routines.

This module use-associates dictionaries_base and re-exports its core public routines and types, including dictionary, dict_init, dict_free, dict_len, dict_size, dict_key, dict_item, and dict_value. Client code should normally write use dictionaries; a separate use dictionaries_base is needed only for low-level code that intentionally avoids the higher-level dependencies.

Quick access

Routines:

f_err_throw()

Needed modules

Subroutines and functions

interface  dictionaries/f_err_throw(message[, err_msg, err_id, err_name, callback, callback_data])
Options:
  • err_msg [character(len=*), in,] :: < error message

  • err_id [integer, in,] :: < The code of the error to be raised.

  • err_name [character(len=*), in,] :: < error name

  • callback [external]

  • callback_data [integer, in,] :: < ??? not really sure

Parameters:

message [f_string, in] :: < error message

Called from:

f_err_raise()

interface  dictionaries/f_err_raise(condition, err_msg[, condition, err_msg, err_id, err_name, callback, callback_data])
Options:
  • condition [logical, in,] :: < the condition which raise the error

  • err_msg [character(len=*), f_string, in,] :: < error message

  • err_id [integer, in,] :: < the code of the error to be raised.

  • err_name [character(len=*), in,] :: < error name

  • callback [external] :: < action to be performed ???

  • callback_data [integer, in,] :: < ??? not really sure

subroutine  dictionaries/f_err_define(err_name, err_msg, err_id[, err_action, callback, callback_data])
Parameters:
  • err_name [character(len=*), in]

  • err_msg [character(len=*), in]

  • err_id [integer, out]

Options:
  • err_action [character(len=*), in,]

  • callback [external]

  • callback_data [integer, in,]

function  dictionaries/f_err_check([err_id, err_name])
Options:
  • err_id [integer, in,]

  • err_name [character(len=*), in,]

Return:

f_err_check [logical]

function  dictionaries/f_err_raise([condition, err_msg, err_id, err_name, callback, callback_data])
Options:
  • condition [logical, in,]

  • err_msg [character(len=*), in,]

  • err_id [integer, in,]

  • err_name [character(len=*), in,]

  • callback [external]

  • callback_data [integer, in,]

Return:

f_err_raise [logical]

Use :

yaml_strings (yaml_toa())

function  dictionaries/f_get_error_dict([ierror])
Options:

ierror [integer, in,]

Return:

f_get_error_dict [dictionary, pointer]

function  dictionaries/f_get_no_of_errors()
Return:

f_get_no_of_errors [integer]

function  dictionaries/f_get_past_error(ierr_num[, add_msg])
Parameters:

ierr_num [integer, in]

Options:

add_msg [character(len=*)]

Return:

f_get_past_error [integer]

function  dictionaries/f_get_last_error([add_msg])
Options:

add_msg [character(len=*), out,]

Return:

f_get_last_error [integer]

subroutine  dictionaries/f_err_clean()
function  dictionaries/f_err_pop([err_id, err_name, add_msg])
Options:
  • err_id [integer, in,]

  • err_name [character(len=*), in,]

  • add_msg [character(len=*), out,]

Return:

f_err_pop [integer]

subroutine  dictionaries/f_err_open_try()
subroutine  dictionaries/f_err_close_try([exceptions])
Options:

exceptions [dictionary, out,pointer]

function  dictionaries/f_err_trying()
Return:

f_err_trying [logical]

function  dictionaries/f_get_error_definitions()
Return:

f_get_error_definitions [dictionary, pointer]

Defining a new error

New error families are registered with f_err_define(), which returns a stable integer code (err_id) that can be reused for subsequent raises and checks. An optional per-error callback and action string may be attached at definition time. Errors should be defined once, at library startup, together with the dictionary errors registered by dictionaries_errors().

Raising and checking

f_err_raise() is the primary idiom for conditional errors: it returns .true. when the condition holds so it can be used inline as:

if (f_err_raise(condition, 'message', err_id=MY_ERR)) return

f_err_throw() raises an error unconditionally (for example inside a routine that has already detected the failure). The raised error is then inspected with f_err_check(), which accepts an optional err_id or err_name to query a specific family, or no argument to ask whether any error is pending. Pending errors are cleared with f_err_clean().

Try/catch environment

f_err_open_try() and f_err_close_try() delimit a nested try/catch region. Inside such a region callbacks are suppressed (errors are simply accumulated) and a stack of pending-error dictionaries is kept so that regions can nest. f_err_check() should be called before f_err_close_try(), and f_err_close_try() optionally returns the pending exceptions as a dictionary for inspection. f_err_trying() reports whether the code is currently inside a try environment.

Inspecting the error record

Each raised error is stored as a dictionary entry, so the introspection API is dictionary-based:

Callback and severe-error policy from exception_callbacks

These routines govern what happens when an error is raised outside a try environment. They live in exception_callbacks (source file callbacks.f90) and are made available alongside the dictionaries API; client code that overrides the default behaviour adds use exception_callbacks explicitly.

Quick access

Routines:

f_dump_last_error(), f_dump_possible_errors(), f_err_ignore(), f_err_set_all_errors_callback(), f_err_set_callback(), f_err_set_last_error_callback(), f_err_severe(), f_err_severe_override(), f_err_severe_restore(), f_err_unset_all_errors_callback(), f_err_unset_callback(), f_err_unset_last_error_callback()

Needed modules

  • f_precisions: Defines the portable Futile kind policy, undefined sentinels, precision conversion helpers, and low-level address inquiry helpers. Host codes should use these names instead of compiler-specific kind

Subroutines and functions

interface  exception_callbacks/f_err_set_callback()

Generic interface to install a global error callback.

The simple overload takes the callback procedure; the advanced overload also forwards a data address to it. The callback is invoked for every error raised outside a try environment. Clear it with f_err_unset_callback().

subroutine example_f_err_set_callback()
  external :: counting_callback

  callback_count = 0
  call f_err_set_callback(counting_callback)
  call f_err_clean()
  call f_err_throw('error triggering the callback', err_name='GENERIC_ERROR')
  call f_err_clean()
  call f_err_unset_callback()
  call require(callback_count == 1, &
       'f_err_set_callback callback was not invoked once')
end subroutine example_f_err_set_callback
subroutine  exception_callbacks/f_err_unset_callback()

Remove the global error callback.

Resets the callback and its data address, restoring the default behaviour for errors raised outside a try environment.

subroutine  exception_callbacks/f_err_severe_override(callback)

Redirect the severe-error policy.

Installs a custom routine to be called by f_err_severe() instead of the default hard stop. Restore the default with f_err_severe_restore().

Parameters:

callback [external]

subroutine  exception_callbacks/f_err_severe_restore()

Restore the default severe-error policy.

Undoes a previous f_err_severe_override().

subroutine  exception_callbacks/f_err_ignore()

No-op callback that silently ignores errors.

Convenience callback for f_err_set_callback() when errors should be swallowed without any output.

subroutine  exception_callbacks/f_err_severe()

Routine of last resort for unrecoverable errors.

Calls the overridden severe callback when one has been installed with f_err_severe_override(), otherwise calls the internal routine that dumps the last error and stops execution.

subroutine  exception_callbacks/f_dump_last_error()

Dump information about the last raised error.

Invokes the callback installed with f_err_set_last_error_callback(), if any.

subroutine  exception_callbacks/f_dump_possible_errors(msg)

Dump the catalogue of all defined error families.

Invokes the callback installed with f_err_set_all_errors_callback(), passing a descriptive message. Used internally to help diagnose unknown error names or ids.

Parameters:

msg (*) [character, in]

subroutine  exception_callbacks/f_err_set_last_error_callback(callback)

Install the last-error dump callback.

The callback is invoked by f_dump_last_error(). Clear it with f_err_unset_last_error_callback().

Parameters:

callback [external] :: < Error routine which will be called

subroutine  exception_callbacks/f_err_unset_last_error_callback()

Remove the last-error dump callback.

subroutine  exception_callbacks/f_err_unset_all_errors_callback()

Remove the all-errors dump callback.

subroutine  exception_callbacks/f_err_set_all_errors_callback(callback)

Install the all-errors dump callback.

The callback is invoked by f_dump_possible_errors() with a descriptive message. Clear it with f_err_unset_all_errors_callback().

Parameters:

callback [external]

Global callbacks

f_err_set_callback() installs a routine invoked each time an error is raised outside a try environment; the advanced overload also accepts a data address to forward to the callback. The callback is cleared with f_err_unset_callback(), which restores the default behaviour. A convenience no-op callback f_err_ignore() is provided to silently swallow errors.

Severe-error policy

f_err_severe() is the routine of last resort, called internally when the library cannot proceed (for example when the error pipeline is used before f_lib_initialize()). Its behaviour can be redirected with f_err_severe_override() (useful to turn a hard stop into a custom cleanup path) and restored with f_err_severe_restore().

Error-dump callbacks

Two separate callback hooks control how errors are reported:

Examples

The examples below are rendered from fortranliteral directives placed in the Fortran source comments next to each public routine (see the policy in the sphinxfortran-documentation skill). They are drawn from the CI-compiled program tests/flib/errors_examples.f90, so the rendered documentation and the tested behaviour stay synchronized.

A larger, integration-style example exercising error definition, conditional raising, nested try/catch and callbacks together is available in the regression test. The Use block shows the module imports needed by client code:

  use dictionaries
  use yaml_strings, only: f_char_ptr

The Error Define block shows how to register error families with f_err_define() and attach per-error callbacks:

  call f_err_define(err_name='ERR_TOTO',&
       err_msg='This is the error message for the error of kind 1 and it is written extensively'//&
       ' on purpose to see whether yaml module prints it',&
       err_action='For this error, contact the routine developer at mail at univ dot gov',&
       err_id=ERR_TOTO,callback=abort_toto)

  call f_err_define(err_name='ERR_TITI',err_msg='test2',err_id=ERR_TITI,&
       callback=abort_titi,callback_data=f_loc(ival))

  call f_err_define(err_name='ERR_GRAVE',err_msg='test2',err_id=ERR_GRAVE,&
       callback=f_err_severe)

Documentation coverage

The CI documentation contract for this page mirrors the one described for f_precisions and checks three complementary notions of coverage:

symbol coverage

Every expected public error-handling entry must appear in the rendered HTML page. This catches missing raise/throw/check/try/callback symbols when the parser or the source changes.

topic coverage

Required explanatory topics must also appear in the rendered page. For the error-handling page these cover the use dictionaries entry point, the error_handling.f90 source location, error definition, raising and checking, the try/catch environment, error-record inspection, the callback and severe-error policy from exception_callbacks, and the fortranliteral example machinery.

example coverage

Public helpers include examples with fortranliteral directives taken from executable code under tests/. The same code is compiled and run by CI, so the rendered documentation cannot silently drift away from tested API behaviour. For parsed helpers and interfaces the directive lives in the Fortran source comment next to the public API. If a public routine has no runnable example the page and source comment should explain why, and the coverage check should make that exception explicit.

The coverage check lives in doc/check_errors_public_api.py and should be extended whenever a new public error-handling routine is added.