Error Handling

Fortran has no exceptions. Error handling is a ladder: catch I/O errors with 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.

Reading the runtime's answer. With -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.