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.
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.
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 callserror 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.
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
subroutine routine_with_possible_error(..., error)
:
type(fatal_error), allocatable, intent(out) :: error
:
end subroutine routine_with_possible_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]
subroutine routine_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")
return
end subroutine routine_with_possible_error
If you happen to use Fypp, you can further simplify the code, by writing [examples/catch_fypp.fpp]:
subroutine routine_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")
end subroutine routine_with_possible_error
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]:
subroutine routine_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
if (allocated(error)) return
print "(a)", "Apparently no error occured"
:
end subroutine routine_propagating_error
Again, you can use some Fypp magic to be more descriptive [examples/propagate_error_fypp.fpp]:
subroutine routine_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"
:
end subroutine routine_propagating_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)) then
call 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]:
subroutine main()
type(fatal_error), allocatable :: error
call routine_with_possible_error(..., error)
call catch_error(error, error_handler)
:
contains
subroutine error_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, ")"
end subroutine error_handler
end subroutine 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
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:
subroutine routine_rethrowing_error(error)
type(fatal_error), allocatable, intent(out) :: error
call routine_throwing_error(error)
if (allocated(error)) then
call error%deactivate()
:
! Rethrowing error
call error%activate()
return
end 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
subroutine routine_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
:
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]:
subroutine routine_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
end subroutine 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:
#0 0x7f5a2fb30d21 in ???
#1 0x7f5a2fb31869 in ???
#2 0x7f5a2fb32f97 in ???
#3 0x55e8d176876a in __errorfx_MOD_fatal_error_final
at errorfx/src/errorfx.f90:125
#4 0x55e8d1767abb in __errorfx_MOD___final_errorfx_Fatal_error
at errorfx/src/errorfx.f90:196
#5 0x55e8d176638a in 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:
#0 0x7fd723fe5d21 in ???
#1 0x7fd723fe6869 in ???
#2 0x7fd723fe7f97 in ???
#3 0x559f121b279f in __errorfx_MOD_fatal_error_final
at errorfx/src/errorfx.f90:125
#4 0x559f121b1af0 in __errorfx_MOD___final_errorfx_Fatal_error
at errorfx/src/errorfx.f90:196
#5 0x559f121b03bf in main
at errorfx/examples/fail_uncaught_fypp.fpp:20
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 = -1
character(:), 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.
pure subroutine create_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)
end subroutine create_io_error
!> Initializes an io_error instance.
pure subroutine io_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 if
if (present(filename)) then
this%filename = filename
end if
end subroutine io_error_init
!> Destroys an error explicitely (after deactivating it)
subroutine destroy_io_error(this)
type(io_error), allocatable, intent(inout) :: this
if (allocated(this)) then
call this%deactivate()
deallocate(this)
end if
end subroutine destroy_io_error
!> Catches an io_error and executes an error handler
subroutine catch_io_error(error, errorhandler)
type(io_error), allocatable, intent(inout) :: error
interface
subroutine errorhandler(error)
import :: io_error
implicit none
type(io_error), intent(in) :: error
end subroutine errorhandler
end interface
call error%deactivate()
call errorhandler(error)
deallocate(error)
end subroutine catch_io_error
!> Catches a generic error class and executes an error handler
subroutine catch_io_error_class(error, errorhandler)
class(fatal_error), allocatable, intent(inout) :: error
interface
subroutine errorhandler(error)
import :: io_error
implicit none
class(io_error), intent(in) :: error
end subroutine 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 if
end subroutine 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
subroutine handle_io_error(error)
class(io_error), intent(in) :: error
print "(2a)", "IO Error found: ", error%message
end subroutine handle_io_error
! Handler for linalg error
subroutine handle_linalg_error(error)
class(linalg_error), intent(in) :: error
print "(2a)", "Linear algebra error found: ", error%message
end subroutine 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:
subroutine routine_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)
return
print "(a)", "you should not see this as an error was thrown before"
end subroutine routine_throwing_error
When using Fypp, it reduces to
subroutine routine_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"
end subroutine routine_throwing_error