Error Handling
iostat, stop deliberately with error stop and a code, guard arithmetic with the IEEE modules, and turn the compiler's runtime checks into a debug safety net. The ladder's top rung is discipline — every failure path says which failure and where it happened.
Exit codes: the contract with the shell
Every program ends with an integer status visible to the calling shell. stop ends with code 0 (success); stop 42 or error stop 42 reports failure. In pipelines and job schedulers that integer is how automation learns your program failed — zero means "continue the pipeline". A discipline worth adopting: reserve small codes for known failure classes and document them in a module parameter.
program exit_codes
implicit none
real :: x
print '(a)', 'Give me a positive number:'
read *, x
if (x <= 0.0) then
error stop 2 ! 2 = invalid input, by this program's contract
end if
print '(a,f8.3)', 'sqrt = ', sqrt(x)
end program exit_codes ! implicit stop 0 on normal end
In a shell: ./exit_codes; echo $? prints the code — 0 for success, 2 for the guarded failure above. On Windows the equivalent is echo %ERRORLEVEL%.
I/O errors: iostat and iomsg
Every I/O statement can carry iostat=ios, iomsg=msg. A nonzero ios means the statement failed; the sign tells the story — negative values are end-of-file or end-of-record conditions (normal), positive values are real errors. The message in msg is human-readable and compiler-localized. The files lesson looped a whole file with this pattern; here it guards a single config read:
program safe_read
implicit none
integer :: unit, ios
real :: tol
character(len=128) :: msg
open (newunit=unit, file="settings.txt", status="old", iostat=ios, iomsg=msg)
if (ios /= 0) then
print '(a)', trim(msg)
error stop 1
end if
read (unit, *, iostat=ios, iomsg=msg) tol
if (ios /= 0) then
print '(a,i0,a)', 'read failed: ', ios, ' ' // trim(msg)
error stop 2
end if
print '(a,es10.3)', 'tolerance = ', tol
close (unit)
end program safe_read
Runtime checks: the debug safety net
Array out-of-bounds access is the classic Fortran memory bug — unchecked by default for speed, instantly diagnosable when checked. Compile debug builds with -g -fcheck=bounds -fbacktrace -Wall -Wextra -pedantic (gfortran), and the runtime halts with an exact procedure, line and variable the moment an index escapes. Release builds drop the checks and add -O3; the two build profiles are how you search for bugs in the careful direction and run fast in the other.
# debug profile — catches the off-by-one immediately
gfortran -g -fcheck=bounds -fbacktrace -Wall -Wextra -o app app.f90
# release profile — the same code, maximum speed
gfortran -O3 -funroll-loops -march=native -o app app.f90
Arithmetic traps with IEEE modules
Floating-point fails softly: division by zero yields Infinity, and 0/0 yields NaN, many lines after the actual fault. The ieee_arithmetic and ieee_exceptions modules expose the hardware flags so a program can ask whether an operation produced an overflow or a divide-by-zero — the honest guard for code that must not silently degrade:
program ieee_guard
use, intrinsic :: ieee_arithmetic
use, intrinsic :: ieee_exceptions
implicit none
real :: a = 1.0e38, b = 1.0e38, c
logical, parameter :: halting = .false.
call ieee_set_halting_mode(ieee_overflow, halting) ! don't abort on overflow
c = a * b ! overflows to Infinity
if (ieee_is_finite(c)) then
print '(a)', 'finite result'
else
print '(a)', 'overflow detected — result is not finite'
end if
if (ieee_support_halting(ieee_divide_by_zero)) then
print '(a)', 'divide-by-zero hardware trap is available on this CPU'
end if
end program ieee_guard
Related intrinsics: ieee_is_nan(x) and ieee_class(x) classify a value precisely; the performance lesson folds NaN-detection into a correctness pipeline.
Assertions and the fail-fast habit
Fortran has no built-in assert; the pattern is an if that aborts with a message when a precondition breaks. A tiny module procedure you write once and reuse everywhere — or stdlib's assert — turns invariant violations into early, located failures instead of mid-simulation corruption:
module checks
implicit none
contains
subroutine assert(cond, what)
logical, intent(in) :: cond
character(len=*), intent(in) :: what
if (.not. cond) then
print '(a)', 'ASSERTION FAILED: ' // what
error stop 3
end if
end subroutine assert
end module checks
program assert_demo
use checks
implicit none
integer :: n = -5
call assert(n > 0, 'iteration count must be positive')
print *, n
end program assert_demo
Place assertions at procedure entries (validating intent(in) arguments) and after allocations and reads. The cost is a comparison; the payoff is a stack trace pointing at the first violated contract instead of an hour of NaN archaeology.
-fbacktrace compiled in, a crash prints the call chain: top line is the failing procedure, below is every caller. Work backwards from the top — the culprit is usually the argument passed into that procedure. The testing and CI lesson in the ecosystem phase turns these habits into automated regression suites.