Skip to content

Latest commit

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

ErrorFx

Fortran library providing exception like error handling using modern Fortran. The error handling mechanism it implements is

  • robust: an uncaught error stops the code automatically,
  • convenient: propagation of errors upwards through the call stack is easily possible,
  • pure: it only requires Fortran 2008 constructs.

It is written in Fortran 2008. If your project uses an appropriate preprocessor (such as the Fypp preprocessor), preprocessor macros can be used to make the error handling code even more compact.

Several examples demonstrating various error handling scenarios can be found in the examples/ folder. Files ending with *.f90 contain the pure Fortran examples, why files ending with *_fypp.fpp their simplified equivalent using Fypp-constructs.

The project is released under the BSD 2-clause license.

Building

Use the usual CMake workflow to build the library and the examples:

mkdir build
cd build
cmake ..
make

The executables built from the examples can be found in the ./examples folder in the build directory.

By default, the Fypp-based examples are included when building the project. In case you want to build without Fypp, pass the -DWITH_FYPP=NO option to CMake.

General concept

The general recipe for the exception like error handling in Fortran is can be formulated as follows:

  • Define a derived type which allows to store the relevant information about your critical errors. Add an activation flag with .false. (deactivated) as default value. Add a finalizer, which calls error stop if it is called for an activated instance.
  • Create an allocatable (but non-allocated) instance of the error type and pass it to the routine which may throw a fatal error. The corresponding dummy argument should have the allocatable, intent(out) attributes. The allocation status of the variable on return will signalize the error status (non-allocated: no error, allocated: error).
  • If an error occurs (within the called routine), allocate the error variable, fill it up with the necessary information, set its activation flag to .true. and return.
  • In the calling routine, check the allocation status of the error variable immediately after the call. If it is allocated (error occured), either propagate it further upwards or handle it locally. If you handle it locally, deactivate the error (by setting its activation flag to .false.), execute the handling code and then finally deallocate the error.

This mechanism provides a very flexible and robust error handling. Errors can be easily propagated upwards to the top level (as it should be the case for robust libraries). Additionally, it ensures, that errors are not overlooked by mistake, as an activated error stops the code when it goes out of scope.

Implementation details

Error type

The error type in ErrorFx is type(fatal_error) [src/errorfx.f90]. When a subroutine is called, which may throw a critical error, the code in the caller would look as

type(fatal_error), allocatable :: error
:
call routine_with_possible_error(..., error)

while the routine possible raising the error would look as

subroutineroutine_with_possible_error(..., error)
:
type(fatal_error), allocatable, intent(out) :: error
:
endsubroutine routine_with_possible_error

Throwing an error

Throwing an error consists of allocation, filling information, activation and returning as explained above. ErrorFx offers convenience functions to make this as simple as possible, so that you only have to write [examples/catch.f90]

subroutineroutine_with_possible_error(..., error)
:
type(fatal_error), allocatable, intent(out) :: error
:
! Creating and throwing an error (activation included)
call create_error(error, message="Error created in routine_with_possible_error")
returnendsubroutine routine_with_possible_error

If you happen to use Fypp, you can further simplify the code, by writing [examples/catch_fypp.fpp]:

subroutineroutine_with_possible_error(..., error)
:
type(fatal_error), allocatable, intent(out) :: error
:
! Creating and throwing an error (activation included)
@:throw_error(error, message="Error created in routine_with_possible_error")
endsubroutine routine_with_possible_error

Propagating an error upwards

If a called routine signalizes an error, you can either handle it in the caller or propagate it further upwards. The propagation happens by simply returning if the error is allocated. Of course, the routine propagating the error upwards must itself have a corresponding error dummy argument [examples/propagate_error.f90]:

subroutineroutine_propagating_error(..., error)
:
type(fatal_error), allocatable, intent(out) :: error
:
call routine_with_possible_error(..., error)
! If error happend, we propagate it upwards, otherwise we continueif (allocated(error)) returnprint"(a)", "Apparently no error occured"
:
endsubroutine routine_propagating_error

Again, you can use some Fypp magic to be more descriptive [examples/propagate_error_fypp.fpp]:

subroutineroutine_propagating_error(..., error)
:
type(fatal_error), allocatable, intent(out) :: error
:
call routine_with_possible_error(..., error)
! If error happend, we propagate it upwards, otherwise we continue
@:propagate_error(error)
print"(a)", "Apparently no error occured"
:
endsubroutine routine_propagating_error

Catching an error

If you do not want to propagate the error upwards, you have to handle it locally, deactivate it (and eventually also deallocate it). The corresponding catching pattern in ErrorFx would look as [examples/catch.f90]

call routine_with_possible_error(..., error)
if (allocated(error)) thencall error%deactivate()
! Do whatever is needed to resolve the error
print"(a,a,a,i0,a)", "Fatal error found: '", error%message, "' (code: ", error%code, ")"
deallocate(error)
end if

Doing the deactivation (call error%deactivate()) as the very first step warranties, that the pattern works also in those cases, where you leave the scope (e.g. via return) during the error handling. If the error handling code does not leave the scope, you can do the deactivation and deallocation together at the end of the error handling block using the convenience routine destroy_error():

call routine_with_possible_error(..., error)
if (allocated(error)) then
! Do whatever is needed to resolve the error
! Make sure you do not leave the scope, as the error is still active!
print"(a,a,a,i0,a)", "Fatal error found: '", error%message, "' (code: ", error%code, ")"
! Deactivate and destroy in one step
call destroy_error(error)
end if

As the "manual" error handling is somewhat error prone (you may forget to deactivate or deallocate), ErrorFx offers you the possibility to handle the error via a dedicated (internal or external) subroutine. The library will first deactivate the error, then call the error handling routine and finally deallocate the error [examples/catch.f90]:

subroutinemain()
type(fatal_error), allocatable :: error
call routine_with_possible_error(..., error)
call catch_error(error, error_handler)
:
containssubroutineerror_handler(error)
type(fatal_error), intent(in) :: error
! Do whatever is needed to resolve the error
! (Deactivation/deallocation is done by the library automatically.)
print"(a,a,a,i0,a)", "Fatal error found: '", error%message, "' (code: ", error%code, ")"endsubroutine error_handlerendsubroutine main

The error handler routine can be an arbitrary subroutine, which takes the thrown error type as intent(in) argument. If it is an internal subroutine, it will even have access to all variables of the hosting scope (e.g. error_handler() can access all variables defined in main() above).

Finally, with Fypp you can write a compact, robust and descriptive error catching construct even without explicit error handling routines as [examples/catch_fypp.fpp]:

call routine_with_possible_error(..., error)
#:block catch_error("error")
! Do whatever is needed to resolve the error
print"(a,a,a,i0,a)", "Fatal error found: '", error%message, "' (code: ", error%code, ")"
#:endblock

Rethrowing an error

If during error handling of a caught error it turns out, that the error can not be handled locally, the code may either throw (create and propagate) a new error or just rethrow the original one. Latter can be achieved by activating the error again (in case it was deactivated already) and returning:

subroutineroutine_rethrowing_error(error)
type(fatal_error), allocatable, intent(out) :: error
call routine_throwing_error(error)
if (allocated(error)) thencall error%deactivate()
:
! Rethrowing error
call error%activate()
returnend if
:

Note, that if you do not leave the scope via return during the error handling (except when rethrowing the error), the calls error%deactivate() and error%activate() can be omitted.

The compact Fypp based analog would be

subroutineroutine_rethrowing_error(error)
type(fatal_error), allocatable, intent(out) :: error
call routine_throwing_error(error)
#:block catch_error("error")
:
! Rethrowing error
@:rethrow_error(error)
#:endblock
:

Failure due to an uncaught error

If an error is not caught (deactivated), it will trigger an error stop when it goes out of scope. You will get an appropriate error message and given on your compilation flags, you may also obtain some traceback information starting from the location where the error went out of scope [examples/fail_uncaught.f90]:

subroutineroutine_failing_due_unhandled_error()
type(fatal_error), allocatable :: error
call routine_with_possible_error(..., error)
! Error was neither caught nor propagated. It would trigger an error stop at
! the end of the subroutine
endsubroutine routine_failing_due_unhandled_error

Running the above example, you would obtain an error stop with some information:

Stopping due to unhandled critical error
Error message: Error created in routine_with_possible_error
Error code: 0
ERROR STOP
Error termination. Backtrace:
#00x7f5a2fb30d21in ???
#10x7f5a2fb31869in ???
#20x7f5a2fb32f97in ???
#30x55e8d176876ain __errorfx_MOD_fatal_error_final
at errorfx/src/errorfx.f90:125
#40x55e8d1767abbin __errorfx_MOD___final_errorfx_Fatal_error
at errorfx/src/errorfx.f90:196
#50x55e8d176638ain main
at errorfx/examples/fail_uncaught.f90:18

If you use Fypp for the same example [examples/fail_uncaught_fypp.fpp], the error message will be more informative, as it will also contain the propagation path of the error itself, so you will know, where it was triggered and how it was propagated up without going out of scope. Latter can be very useful, if the error was propagated upwards through several levels:

Stopping due to unhandled critical error
Error message: An error occured in routine1()
Error code: 0
Error propagation path:
errorfx/examples/fail_uncaught_fypp.fpp:26
ERROR STOP
Error termination. Backtrace:
#00x7fd723fe5d21in ???
#10x7fd723fe6869in ???
#20x7fd723fe7f97in ???
#30x559f121b279fin __errorfx_MOD_fatal_error_final
at errorfx/src/errorfx.f90:125
#40x559f121b1af0in __errorfx_MOD___final_errorfx_Fatal_error
at errorfx/src/errorfx.f90:196
#50x559f121b03bfin main
at errorfx/examples/fail_uncaught_fypp.fpp:20

Extending errors

Sometimes, it may be desirable to extend the fatal_error type. Either, because you wish to create some errors which carry more information than the base type does, or because you wish to differentiate between errors based on their class (by creating an error class hierarchy as you find for example in Python).

The extension is straightforward. The following example demonstrates, how an I/O error could be introduced, which also contains the filename and the unit associated with the I/O problems. Appart of the type extension, one should also provide convenience function to catch an error of the extended type and of the extended class [examples/error_extension.f90]:

module error_extension
use errorfx, only : fatal_error, fatal_error_init
implicit none
private
public :: io_error, io_error_init, catch_io_error_class
public :: create_error, catch_error, destroy_error
!> Specific I/O error created by extending the general type
type, extends(fatal_error) :: io_error
integer:: unit =-1character(:), allocatable :: filename
end type io_error
!> Error creator (use those routines to create an error in the code)
interface create_error
module procedure create_io_error
end interface create_error
!> Catches specific error types
interface catch_error
module procedure catch_io_error
end interface catch_error
!> Deactivates and deallocates a specific error type
interface destroy_error
module procedure destroy_io_error
end interface destroy_error
contains
!> Creates an IO error.
puresubroutinecreate_io_error(this, code, message, unit, filename)
type(io_error), allocatable, intent(out) :: this
integer, optional, intent(in) :: code
character(*), optional, intent(in) :: message
integer, optional, intent(in) :: unit
character(*), optional, intent(in) :: filename
allocate(this)
call io_error_init(this, code=code, message=message, unit=unit, filename=filename)
endsubroutine create_io_error
!> Initializes an io_error instance.
puresubroutineio_error_init(this, code, message, unit, filename)
type(io_error), intent(out) :: this
integer, optional, intent(in) :: code
character(*), optional, intent(in) :: message
integer, optional, intent(in) :: unit
character(*), optional, intent(in) :: filename
call fatal_error_init(this%fatal_error, code=code, message=message)
if (present(unit)) then
this%unit = unit
end ifif (present(filename)) then
this%filename = filename
end ifendsubroutine io_error_init
!> Destroys an error explicitely (after deactivating it)
subroutinedestroy_io_error(this)
type(io_error), allocatable, intent(inout) :: this
if (allocated(this)) thencall this%deactivate()
deallocate(this)
end ifendsubroutine destroy_io_error
!> Catches an io_error and executes an error handler
subroutinecatch_io_error(error, errorhandler)
type(io_error), allocatable, intent(inout) :: error
interface
subroutineerrorhandler(error)
import :: io_error
implicit none
type(io_error), intent(in) :: error
endsubroutine errorhandler
end interface
call error%deactivate()
call errorhandler(error)
deallocate(error)
endsubroutine catch_io_error
!> Catches a generic error class and executes an error handler
subroutinecatch_io_error_class(error, errorhandler)
class(fatal_error), allocatable, intent(inout) :: error
interface
subroutineerrorhandler(error)
import :: io_error
implicit none
class(io_error), intent(in) :: error
endsubroutine errorhandler
end interface
logical:: caught
if (allocated(error)) then
caught =.false.
select type (error)
class is (io_error)
call error%deactivate()
call errorhandler(error)
caught =.true.
end select
if (caught) deallocate(error)
end ifendsubroutine catch_io_error_class
end module error_extension

Given different extensions of the base type, the patterns to generate and catch the errors change slightly. One would typically use class(fatal_error) variables instead of type(fatal_error). Additionally the select type construct can be used to find out which actual error subclass was thrown. Let's assume that two extending error types io_error and linalg_error had been created, a pattern, which can distinguish between the two would look as [examples/catch_class.f90]:

 class(fatal_error), allocatable :: error
call routine_throwing_error(..., error)
call catch_io_error_class(error, handle_io_error)
call catch_linalg_error_class(error, handle_linalg_error)
contains
! Handler for io error
subroutinehandle_io_error(error)
class(io_error), intent(in) :: error
print"(2a)", "IO Error found: ", error%message
endsubroutine handle_io_error
! Handler for linalg error
subroutinehandle_linalg_error(error)
class(linalg_error), intent(in) :: error
print"(2a)", "Linear algebra error found: ", error%message
endsubroutine handle_linalg_error

Alternatively, with manual deactivation and deallocation without explicit error handler routines:

class(fatal_error), allocatable :: error
call routine_throwing_error(..., error)
if (allocated(error)) then
select type (error)
class is (io_error)
call error%deactivate()
print"(2a)", "IO Error found: ", error%message
class is (linalg_error)
call error%deactivate()
print"(2a)", "Linear algebra error found: ", error%message
class default
print"(a)", "Thrown error had not been handled by this block"
end select
if (.not. error%is_active()) deallocate(error)
end if

Or in the more compact Fypp-form [examples/catch_class_fypp.fpp]:

class(fatal_error), allocatable :: error
call routine_throwing_error(..., error)
#:block catch_error_class("error")
#:contains io_error
print"(2a)", "IO Error found: ", error%message
#:contains linalg_error
print"(2a)", "Linear algebra error found: ", error%message
print"(a,i0)", "Additional info: ", error%info
#:endblock

When the error is created, it should be converted from the specialized type to the generic type, easily accomplished with a move_alloc() statement:

subroutineroutine_throwing_error(error)
class(fatal_error), allocatable, intent(out) :: error
type(io_error), allocatable :: ioerr
call create_error(ioerr, message="Failed to open file", filename="test.dat")
call move_alloc(ioerr, error)
returnprint"(a)", "you should not see this as an error was thrown before"endsubroutine routine_throwing_error

When using Fypp, it reduces to

subroutineroutine_throwing_error(error)
class(fatal_error), allocatable, intent(out) :: error
type(io_error), allocatable :: ioerr
@:throw_error_class(ioerr, io_error, message="Failed to open file", filename="test.dat")
print"(a)", "you should not see this as an error was thrown before"endsubroutine routine_throwing_error

About

Robust exception like error handling using modern Fortran

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages