37 min read

> *"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're

Prerequisites

  • 2
  • 5
  • 6
  • 7
  • 9
  • 11

Learning Objectives

  • Generalize the iostat mechanism from Chapter 7 to memory allocation with stat= and errmsg=, and guard every allocate so a failure is a message rather than an abort.
  • Distinguish stop from error stop, choose meaningful exit codes, and explain how a program reports success or failure to the shell, a Makefile, or a CI system.
  • Turn on the development flags — -fcheck=all, -fbacktrace, and -ffpe-trap — read the runtime diagnostics they produce, and say which to remove for a production build.
  • Locate a logic bug with gdb and a memory error with valgrind, and match each tool to the class of fault it catches.
  • Write assertions and preconditions, and apply defensive patterns that fail early and loudly instead of computing on bad data.
  • Recognize and fix Fortran's classic bugs: uninitialized variables, out-of-bounds access, integer overflow, precision loss, and the implicit-save trap.

Chapter 13: Error Handling, Debugging, and Defensive Programming

"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?" — Brian W. Kernighan and P. J. Plauger, The Elements of Programming Style

Overview

Every program you have written so far in this book has been optimistic. It assumed the config file exists, that the allocation succeeds, that the array index is in range, that the number you divided by is not zero, and that the variable you read had first been set. Those assumptions hold in a demo. They do not hold in a simulation that runs for three days on a cluster, reads a config a colleague edited, sizes a grid from a parameter you typed at 2 a.m., and marches a nonlinear system toward the exact regime where numbers blow up. In real scientific computing, the question is never whether something will go wrong — it is whether your program will tell you, clearly and early, or whether it will sail on computing garbage and hand you a plausible-looking wrong answer a week later.

This chapter is about the difference between those two outcomes, and it is one of the most practically valuable in the book. We will do three related things. First, error handling: how to ask each operation that can fail — an allocation, an I/O statement — whether it succeeded, and how to halt deliberately, with a message and an exit code the outside world understands, when it did not. Second, debugging: the compiler flags that turn silent corruption into a loud diagnostic, and the two tools — gdb and valgrind — that let you watch a program from the inside and catch it touching memory it should not. Third, defensive programming: the habit of writing code that checks its own assumptions, so a bug is caught at its source instead of ten thousand timesteps downstream. None of this is glamorous, and all of it is what separates a script from an instrument.

In this chapter, you will learn to:

  • Generalize the iostat idea from Chapter 7 to allocation with stat and errmsg, and guard every allocate.
  • Halt with error stop instead of stop, choose an exit code, and report failure to the shell and to the automation that runs your code.
  • Compile with -fcheck=all, -fbacktrace, and -ffpe-trap to catch out-of-bounds accesses, get a stack trace, and trap a floating-point exception the instant a NaN is born.
  • Drive gdb to a breakpoint and inspect a live Fortran array, and run valgrind to find a leak or a dangling read.
  • Write an assertion, check preconditions, and adopt the defensive-programming patterns that keep numerical code honest.
  • Name and disarm the usual suspects: uninitialized variables, out-of-bounds indices, integer overflow, precision loss, and the notorious implicit save.

Learning Paths

How to read this chapter by track. - 🔬 Scientist ("I need a right answer, not a plausible one") — §13.1 (guard allocations), §13.5 (assertions and preconditions), and §13.6 (the classic bugs) are your core. Skim the gdb/valgrind mechanics in §13.4; know they exist and return when you need them. - 📖 Standard — read straight through. Error handling is the one topic in Part II that no track should skip, as the part introduction warns. - 🔧 Legacy ("I inherited old code") — §13.6 is your survival kit: uninitialized variables and implicit save are endemic in old code, and §13.4's debuggers are how you understand a program nobody documented. - ⚡ HPC ("my run is expensive and long") — §13.2 (error stop halts all images in a parallel run) and §13.3 (-ffpe-trap to catch a NaN early in a week-long simulation) are yours; and note where §13.5 says to remove the checks for production.


13.1 Errors as Values: iostat Revisited, stat/errmsg Introduced

Fortran's philosophy of error handling is refreshingly plain, and you have already met half of it. Many languages signal failure by throwing something — an exception that unwinds the call stack until someone catches it. Fortran, with a few deliberate exceptions we will meet, does the opposite: an operation that can fail offers you an output that reports whether it did, and hands control straight back to you. You ask; you decide. No hidden control flow, nothing unwinding behind your back — which, not coincidentally, is exactly the sort of predictability the optimizer loves.

You saw the pattern in Chapter 7: the iostat/iomsg specifiers turn a fatal I/O error into a value you can test. iostat=ios sets ios to zero on success, a negative value at end-of-file, and a positive value on an error, and — crucially — lets the program continue; iomsg=msg fills a string with a human-readable description. That is the shape of all Fortran error handling, and this chapter's first job is to point out that the same shape appears wherever an operation can fail. The first place beyond I/O is memory allocation.

An allocate can fail. The obvious way is running out of memory — you asked for a grid larger than the machine has — but there are quieter ways too, such as allocating an array that is already allocated. By default, a failed allocate aborts your program with a terse runtime message, which is exactly the behavior you do not want in a long run that has other things it could do (fall back to a smaller grid, write a checkpoint, report cleanly). The fix is the same idea as iostat, spelled stat and errmsg.

Definition (stat and errmsg). Optional specifiers on allocate and deallocate that turn a fatal memory error into a reportable value — the allocation counterpart of iostat/iomsg. stat=s sets the integer s to zero on success and a nonzero, processor-dependent value on failure (out of memory, an already-allocated object, an already-deallocated one), and lets the program continue instead of aborting. errmsg=msg fills the character variable msg with a description of the failure. This is "the iostat mechanism from Chapter 7, generalized" to memory: the same ask-then-decide pattern, a different verb.

Here is the guarded pattern in full. We allocate a field, check the status, and — to show a deterministic failure you can reproduce — deliberately allocate the same array a second time, which is an error condition the runtime reports through stat rather than aborting:

program allocation_stat
  use, intrinsic :: iso_fortran_env, only: dp => real64, error_unit
  implicit none
  real(dp), allocatable :: big(:)
  integer :: stat
  character(len=256) :: errmsg

  ! First allocation, guarded. A grid too large for memory would set stat /= 0
  ! HERE, with a message, instead of aborting the program.
  allocate(big(1000000), stat=stat, errmsg=errmsg)
  if (stat /= 0) then
     write(error_unit, '(a)') 'first allocate failed: ' // trim(errmsg)
     error stop 3
  end if
  print '(a, i0)', 'first allocate : ok, size = ', size(big)

  ! Allocating an ALREADY-allocated variable is an error condition. Without stat=
  ! this line would abort; with it, we catch it and carry on.
  allocate(big(2000000), stat=stat, errmsg=errmsg)
  if (stat /= 0) then
     print '(a)', 'second allocate: FAILED (already allocated)'
     write(error_unit, '(a)') '  message (stderr): ' // trim(errmsg)
  end if

  deallocate(big, stat=stat)
  print '(a, i0)', 'deallocate stat = ', stat
end program allocation_stat
$ gfortran -std=f2018 -Wall -O2 example-01-allocation-stat.f90 -o alloc && ./alloc
first allocate : ok, size = 1000000
second allocate: FAILED (already allocated)
deallocate stat = 0

The three lines on standard output are fully determined: the first allocation succeeds, so size(big) is one million; the second fails because big is already allocated, so we report it and leave big untouched; the deallocate then succeeds with stat = 0. The one thing we do not pin down is the text of errmsg, which we route to standard error. Its wording is processor-dependent — a representative gfortran message is Attempting to allocate already allocated variable 'big' — and a program should print such text but never branch on it. Branch on the integer stat; show the string to a human.

Notice two habits already on display, both of which the rest of the chapter formalizes. Diagnostics go to standard error (error_unit from iso_fortran_env), not standard output, so a user piping your results to a file still sees the complaint on the terminal and does not find an error message wedged into their data. And a genuine failure ends in error stop with an integer — the deliberate, informative halt we turn to next.

🔗 Connection: the symmetry is worth fixing in your mind because it repeats. I/O uses iostat/iomsg (Chapter 7); allocation uses stat/errmsg; and in the parallel chapters the coarray statements (Chapter 32) take a stat= too, with named constants like stat_failed_image for a node that has died mid-run. One pattern — ask whether it worked, then decide — covers I/O, memory, and distributed execution. Learn it once here.

⚠️ Common Pitfall: the value the runtime puts in stat on failure is processor-dependent, exactly like the positive iostat error values of Chapter 7. Never write if (stat == 151) … against a magic number you saw once; the only portable test is if (stat /= 0). If you need to distinguish kinds of allocation failure, the iso_fortran_env module supplies the named constant stat_failed_image (and, for stopped images, stat_stopped_image) for the coarray cases; for ordinary allocation, nonzero-means-failure is all the standard promises.


13.2 stop vs error stop, and the Exit Code

When a program decides it cannot sensibly continue, it must stop — but how it stops carries information that the world outside the program reads. A program is not an island; it is a process launched by a shell, a Makefile, a batch scheduler, or a continuous-integration runner, and when it ends it hands that launcher a single small integer: its exit code. By universal convention, zero means success and any nonzero value means failure. Getting this right is the difference between a nightly pipeline that notices your solver crashed and one that cheerfully feeds its garbage output to the next stage.

Fortran gives you two ways to terminate on purpose. You have used stop; now meet its serious sibling.

Definition (error stop). error stop terminates the program immediately as an error termination, reporting failure to the environment with a nonzero exit code. Plain stop is a normal termination with a zero exit code by default. Both accept an optional stop code — an integer or a character constant — which error stop uses as (or maps to) the process's exit status and which the runtime also echoes to standard error. The two differ in intent and, in a parallel program, in force: error stop halts every image at once, where stop is a more orderly collective shutdown (Chapter 32).

Here is the whole idea in a program small enough to hold in your head. It validates a parameter that, in the real solver, would have come from a namelist, and refuses to run on a nonsensical value:

program exit_codes
  use, intrinsic :: iso_fortran_env, only: error_unit
  implicit none
  integer :: n_steps

  n_steps = -5                      ! pretend this arrived from a config file

  if (n_steps <= 0) then
     write(error_unit, '(a, i0)') 'fatal: n_steps must be positive, got ', n_steps
     error stop 2                   ! deliberate error termination, exit code 2
  end if

  print '(a)', 'run complete'       ! (not reached in this run)
end program exit_codes

Compile it, run it, and ask the shell what exit code it produced. Because the runtime also prints its own ERROR STOP line to standard error, you see two messages — yours and the system's — and then the code:

$ gfortran -std=f2018 -Wall exit_codes.f90 -o exit_codes
$ ./exit_codes
fatal: n_steps must be positive, got -5
ERROR STOP 2
$ echo $?
2

Everything here is deliberate. Our diagnostic — what went wrong, with the offending value — goes to standard error. Then error stop 2 ends the process with exit code 2, which echo $? reads back. (The ERROR STOP 2 line is emitted by the gfortran runtime; its exact form is version-dependent, so treat it as representative — but the exit code you chose is not.) Had we written a bare stop instead, the program would have ended with code 0, telling the shell "all is well" — a lie that a Makefile would believe.

Why choose the code? Because distinct codes let automation distinguish failure modes without parsing text. A common discipline, which the Project Checkpoint adopts, is to reserve small integers per category — say, 2 for a bad configuration and 3 for an allocation failure — so a wrapper script can branch on $? and react appropriately. One caveat worth knowing: on Unix the exit status is taken modulo 256, so keep your codes in the range 1–255 (a stop 256 would arrive as 0, which would read as success). Small, meaningful, positive codes are the rule.

stop error stop
Termination kind normal error
Default exit code 0 (success) nonzero (gfortran: 1)
With a numeric code N exits with N exits with N
Runtime message none for bare stop echoes ERROR STOP … to stderr
Parallel (coarrays) orderly collective stop immediate, all images
Use it for a successful or early normal end a failure the caller must notice

🚪 Threshold Concept — your program's exit code is a promise to everything that runs it. Beginners think of a program as ending when it prints its last line. Professionals know a program ends by reporting — handing the shell a verdict that scripts, schedulers, and CI systems act on without ever reading the output. error stop with a chosen code is how you tell the truth about failure to a machine. Once you see the exit code as the program's word, you will never end a failed run with a bare stop again, and you will understand why a solver that "worked" but returned code 1 is a solver that told you it did not.

📜 From History: error stop is young — it arrived in Fortran 2008, alongside coarrays, and its parallel semantics are the reason it exists in that standard: with many images running at once, you need a way to say "stop everyone, now, this is fatal," distinct from an orderly stop. FORTRAN 77 had only STOP, and the notion of a meaningful exit code was left to folklore and platform convention. That the language grew a first-class error termination is one more small piece of the theme that modern Fortran is a modern language: it takes its place in a toolchain of shells and build systems seriously.

🐍 Python Comparison: Python signals failure by raising an exception, which unwinds the stack until a try/except catches it — or, if none does, ends the interpreter with a traceback and a nonzero exit code. Fortran has no exceptions: there is no try, no except, no stack unwinding. You get the value returns of §13.1 for recoverable conditions and error stop for fatal ones. This is a deliberate design choice, not a gap — hidden non-local control flow is exactly what a compiler cannot optimize across, so a language built for speed prefers explicit status you test in the open. It is also, in sys.exit(2), the one place Python and Fortran agree: an integer exit code is the lingua franca of the shell.


13.3 The Development Flags: -fcheck, -fbacktrace, -ffpe-trap

The compiler is your most powerful debugging tool, and most of its help is switched off by default because it costs speed. While you are developing, you pay that cost gladly, because the flags turn three whole categories of silent misbehavior into loud, located diagnostics. All three are catalogued in Appendix C; here is what they do and how to read what they say.

-fcheck=all — runtime checks. This flag inserts checks the standard does not require: array-bounds verification, allocation status, pointer association, and more. The headline is bounds checking. Consider an out-of-bounds access, the most common array bug there is:

program bounds_demo
  implicit none
  integer :: a(5), i
  a = 0
  i = 7
  a(i) = 1          ! index 7 into a 5-element array — out of bounds
  print *, a
end program bounds_demo

Compiled plainly, this is undefined behavior: gfortran writes to whatever memory lies past a, and the program may print garbage, crash, or — worst of all — appear to work while quietly corrupting a neighbor. Compiled with the check, it stops at the offending line with a message that names the array, the dimension, and the bound:

$ gfortran -std=f2018 -Wall -fcheck=all -g bounds_demo.f90 -o bounds && ./bounds
At line 6 of file bounds_demo.f90
Fortran runtime error: Index '7' of dimension 1 of array 'a' above upper bound of 5

That text block is a representative gfortran message — the exact wording varies by version, and we did not run the program to obtain it; we are telling you the shape of what you will see. The value is immense: without the flag you have a heisenbug that moves when you look at it; with it, you have a line number.

-fbacktrace — a stack trace on a crash. When a program dies from a signal, this flag prints the chain of procedure calls that led there, so you see not just where but how you arrived. Pair it with -g, which emits the debug information that lets the trace carry file names and line numbers. It costs nothing at run time (it only acts when the program is already crashing), so there is little reason ever to omit it during development.

-ffpe-trap — catch a floating-point exception at birth. This is the subtlest and, for numerical code, the most valuable. By default, IEEE 754 arithmetic (Chapter 20) does not crash on a bad operation — it produces a special value and keeps going. Divide by zero and you get Infinity; compute $0/0$ or $\sqrt{-1}$ and you get a NaN (Not a Number); these then propagate silently through every subsequent calculation, so a single bad operation at step 3 can poison a field that you only notice is NaN at step 30,000. Trapping changes that.

Definition (floating-point exception trap). A floating-point exception is a condition the CPU can raise for an anomalous arithmetic operation: invalid (e.g. $0/0$ or $\sqrt{-1}$, yielding NaN), zero (division of a nonzero by zero, yielding Inf), overflow (a result too large to represent), and the benign-but-constant underflow and inexact. To trap an exception (gfortran's -ffpe-trap=invalid,zero,overflow) is to ask the hardware to halt the program at the exact instruction that raised it, instead of silently producing NaN/Inf. Combined with -fbacktrace, it converts "the field became NaN sometime in the last hour" into "the NaN was created on this line."

The demonstration is a division by zero, shown two ways:

program fpe_demo
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp) :: x, y
  x = 0.0_dp
  y = 1.0_dp / x        ! divide by zero
  print '(a, f0.1)', 'y = ', y
end program fpe_demo

Without trapping, the program runs to completion and prints a special value; with trapping, it stops at line 6, and the backtrace points a finger:

$ gfortran -std=f2018 -Wall fpe_demo.f90 -o fpe && ./fpe
y = Inf
$ gfortran -std=f2018 -Wall -ffpe-trap=invalid,zero,overflow -g -fbacktrace fpe_demo.f90 -o fpe && ./fpe
Program received signal SIGFPE: Floating-point exception - erroneous arithmetic operation.

Backtrace for this error:
#0  ...
#1  ...
    at fpe_demo.f90:6

Again the crash text is representative and version- and platform-dependent — we did not execute the program, and the #0/#1 frames and addresses will differ on your machine — but the essential facts are reliable: the untrapped run prints an infinity and continues, and the trapped run dies at the line that divided by zero. For a solver that can go unstable, this is how you find the first bad step.

⚠️ Common Pitfall — trap the right exceptions. Trap invalid, zero, and overflow; do not add underflow or inexact. Underflow (a result rounding to zero) and inexact (any result that is not representable exactly, which is almost every real operation — recall from Chapter 20 that $0.1$ is not exact in binary) occur constantly in correct code; trapping them would halt a healthy program instantly. A second subtlety: because trapping is a global hardware mode, it can also fire inside a third-party library that deliberately computes with NaN/Inf as sentinels. If a trap fires in code you did not write, that is why — and it is a good reason to keep the trap in your development build and understand exactly what it is telling you.

🧩 Try It Yourself. Type the bounds_demo program above. Compile it without -fcheck and run it — note what it prints (it may look harmless). Now recompile with -fcheck=all -g and run it again. Predict, before you run, what the second version will say and on which line. The gap between "seems fine" and "runtime error, line 6" is precisely the value of the flag, and feeling that gap once will make you reach for -fcheck=all for the rest of your career.

⚡ Performance Note: -fcheck=all is a development tool, not a production one. Bounds checking adds a comparison to every array access, which in a tight stencil loop can cost you a large fraction of your speed. The discipline — spelled out in Chapter 30 — is to build with -fcheck=all -g -O0 while developing and to remove -fcheck for production runs, switching to -O2 or higher. -fbacktrace and -ffpe-trap are cheap enough to keep on in many production builds; -fcheck is the one you take off. That performance is not accidental is a theme of this book, and here it is a direct instruction: know which flags to strip when the run has to be fast.


13.4 Debuggers: gdb for Logic, valgrind for Memory

Flags catch faults at the moment they happen. Sometimes you need to stop time and look around — inspect a variable, step one line at a time, walk back up the call stack. That is a debugger, and for gfortran the two that matter are gdb (for logic and control flow) and valgrind (for memory errors). Neither requires you to change your code; both want you to have compiled with -g (and, ideally, -O0, so the optimizer has not rearranged the code out from under the line numbers).

gdb — watch a program from the inside. You compile with debug info, launch the program under gdb, set a breakpoint where you want to pause, run, and then inspect. gdb understands Fortran: it knows arrays are 1-based, it prints whole arrays and array slices in Fortran syntax, and it shows you locals by name. A session to inspect a solver's field at a chosen step looks like this — presented as a representative transcript, not a run we performed:

$ gfortran -std=f2018 -Wall -g -O0 heat.f90 -o heat
$ gdb ./heat
(gdb) break heat.f90:42          # pause when execution reaches line 42
(gdb) run
Breakpoint 1, heat () at heat.f90:42
42          call step(field, alpha, dt)
(gdb) print nx                   # inspect a scalar
$1 = 5
(gdb) print field%u(1:3, 2)      # a Fortran array slice — 1-based, as you wrote it
$2 = (100, 0, 0)
(gdb) next                       # run the current line, stay in this procedure
(gdb) step                       # like next, but descend INTO a called procedure
(gdb) backtrace                  # who called whom to get here
(gdb) continue                   # resume until the next breakpoint

The moves are always the same handful: break to choose where to stop, run to start, print to look, next/step to advance one line (stepping over or into a call), backtrace (bt) to see the call chain, and continue to let it go. The exact $1`, `$2 output and line numbers above are illustrative; what is reliable is the vocabulary, which is worth committing to memory because it does not change from program to program.

💡 Intuition: -fcheck and gdb answer different questions. -fcheck answers "did something illegal happen, and where?" — it is a smoke detector you leave armed. gdb answers "why is this variable wrong?" — it is you walking through the house with a flashlight after the alarm. Reach for the flag first (it is free to leave on and often names the line outright); reach for the debugger when you know what is wrong but not why.

valgrind — catch memory crimes. Some bugs are not about logic but about memory: reading uninitialized storage, reading or writing past the end of an allocation, using memory after it was freed (the dangling pointer of Chapter 11), or leaking memory you allocated and never freed. valgrind runs your program on a synthetic CPU that watches every memory access and reports each crime with a stack trace. You compile with -g and simply run under it:

$ gfortran -std=f2018 -Wall -g -O0 leaky.f90 -o leaky
$ valgrind --leak-check=full ./leaky
==12345== Invalid read of size 8
==12345==    at 0x... : MAIN__ (leaky.f90:19)
==12345==  Address 0x... is 0 bytes inside a block of size 8 free'd
==12345==    at 0x... : deallocate ...
...
==12345== LEAK SUMMARY:
==12345==    definitely lost: 8 bytes in 1 blocks

That transcript is representative — the addresses and the ==PID== prefix will differ, and we did not run it. What valgrind reliably gives you is the kind of error (an invalid read of freed memory; a definite leak) and the line that committed it. This is the tool that finds the dangling-pointer bug from Chapter 11 — where a pointer still aliases memory freed through another pointer — precisely because that bug passes every test you can write in the language itself.

🔗 Connection: here is one more argument for the "prefer allocatable" rule of Chapter 11. A plain allocatable array is freed automatically when it goes out of scope and can never be aliased, so it cannot leak and cannot dangle — two whole categories of valgrind error that a pointer can produce and an allocatable cannot. The solver's field_t holds its grid in an allocatable component for exactly this reason. When you do use pointers or call into C (Chapter 14), valgrind is your safety net.

⚠️ Common Pitfall: valgrind is thorough but slow — commonly 10–30× slower than a native run (a Tier-2 order of magnitude, not a promise), because it instruments every memory access. Run it on a small test case, not your production grid. For the specific case of stack and global buffer overflows, and for much faster instrumented runs, gfortran also offers -fsanitize=address (AddressSanitizer) and -fsanitize=undefined, which you compile into the program; they catch an overlapping-but-different set of faults at a fraction of valgrind's slowdown. Different tools, different coverage — keep both in the kit.

🔄 Check Your Understanding. 1. You compile with -fcheck=all and the program stops with "Index '7' … above upper bound of 5." Which class of bug is this, and what would the same program most likely have done without the flag? 2. Which tool would you reach for to find a memory leak, and which to understand why a scalar has the wrong value at line 42? 3. Why must you not add underflow to your -ffpe-trap list?

Answers 1. An out-of-bounds array access. Without -fcheck it is undefined behavior — most likely it would write past the array and either print garbage, crash unpredictably, or silently corrupt adjacent memory. 2. valgrind (--leak-check=full) for the leak; gdb (breakpoint at line 42, print the scalar) to see why the value is wrong. 3. Underflow — a result too small to represent, rounding to zero — happens routinely in correct numerical code, so trapping it would halt a healthy program. Trap only invalid, zero, and overflow.


13.5 Assertions, Preconditions, and Defensive Programming

So far we have handled failures the language and runtime hand us. Defensive programming is the deliberate practice of manufacturing your own checks — encoding, in executable form, the beliefs your code depends on, so that a violated belief stops the program at its source instead of silently producing a wrong number far downstream.

Definition (defensive programming). Defensive programming is writing code that actively verifies its own assumptions — validating inputs (preconditions), checking that computed results are sane (postconditions), and guarding against the states it is not built to handle — and that fails early, loudly, and with a diagnostic when an assumption is violated. Its goal is to convert a subtle, far-downstream wrong answer into an immediate, localized, diagnosable halt. The single most useful tool of the discipline is the assertion.

Definition (assertion). An assertion is a statement, placed in the code, that a condition the programmer believes must be true at that point actually is; if it is false, the program halts at once with a message, because a false assertion means a bug has already occurred. Fortran has no built-in assert (unlike C's assert.h), so you write one: a small procedure that does nothing when the condition holds and error stops with a message when it does not.

The assertion procedure is a handful of lines, and once written it serves your whole program:

module asserts
  use, intrinsic :: iso_fortran_env, only: error_unit
  implicit none
  private
  public :: assert
contains
  subroutine assert(condition, message)
    logical,          intent(in) :: condition
    character(len=*), intent(in) :: message
    if (.not. condition) then
       write(error_unit, '(a)') 'ASSERTION FAILED: ' // message
       error stop 1
    end if
  end subroutine assert
end module asserts

Now use it to guard a routine's preconditions (what must be true on entry) and postconditions (what must be true of the result). A function computing a mean depends on a nonempty array, and its result must lie between the smallest and largest inputs — both are beliefs worth asserting:

program defensive_demo
  use asserts, only: assert
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp) :: v(4) = [2.0_dp, 4.0_dp, 6.0_dp, 8.0_dp]
  print '(a, f6.2)', 'mean = ', mean(v)
contains
  function mean(x) result(m)
    real(dp), intent(in) :: x(:)
    real(dp) :: m
    call assert(size(x) > 0, 'mean: array must be non-empty')          ! precondition
    m = sum(x) / real(size(x), dp)
    call assert(m >= minval(x) .and. m <= maxval(x), 'mean: out of range')  ! postcondition
  end function mean
end program defensive_demo
$ gfortran -std=f2018 -Wall -O2 example-02-assertions.f90 -o defensive && ./defensive
mean =   5.00

The mean of $[2,4,6,8]$ is $20/4 = 5.00$, and because $2 \le 5 \le 8$ the postcondition holds, so the program runs to completion and prints the answer. The assertions are invisible when the code is correct — which is the point. But hand mean an empty array and the precondition fires at the entry to mean, not somewhere deep in a division, printing (representatively) ASSERTION FAILED: mean: array must be non-empty and ending with a nonzero exit code. The bug is reported one line from where it lives, instead of surfacing as a mysterious NaN in your output file an hour later.

🚪 Threshold Concept — an assertion is an executable belief. A comment that says "n is always positive here" is a hope; the reader trusts it, the compiler ignores it, and when it quietly stops being true nothing happens until the wrong answer appears. call assert(n > 0, 'n must be positive') is the same belief made executable — it cannot rot silently, because the instant it becomes false the program says so, at that line. This reframes what assertions are for: not to handle errors you expect (that is what stat and iostat are for), but to catch the "impossible" situations that reveal a bug in your own logic. The two are different jobs. Recover from what the world does to you; assert what you believe about yourself.

Defensive programming is broader than the assert call, and the chapter's specific tools all fit under it:

  • Validate at the boundary. Check a configuration or a procedure argument the moment it arrives, before it can contaminate a computation — the Project Checkpoint validates the whole config up front.
  • Guard every operation that can fail. Every open gets an iostat (Chapter 7); every allocate gets a stat (§13.1). No unchecked failure points.
  • Initialize everything. An unset variable is a bug waiting for a platform to change (§13.6); give every variable a value before you read it.
  • Report to standard error, with context. A diagnostic should name what was expected and what was seen — 'n_steps must be positive, got -5' beats 'bad input' every time.

⚡ Performance Note — make your checks removable. Assertions and bounds checks cost cycles, and in a hot loop that cost is real. The professional pattern is to make them vanish in production. One clean way is a compile-time switch: declare logical, parameter :: checking = .true. in a module and write if (checking) call assert(...). When you flip checking to .false., the condition is a compile-time constant, and the optimizer deletes the dead branch entirely — you pay nothing. (The C-and-Python world does the same thing with NDEBUG and python -O; Fortran programmers often reach for the C preprocessor, a .F90 file and #ifndef NDEBUG, for the identical effect.) The discipline mirrors §13.3's rule for -fcheck: check exhaustively while developing, strip the cost for the production run, and — per Chapter 30 — record which build you used.

🔗 Connection: assertions and preconditions are the inline half of correctness; the external half is testing — running the code against known answers, which is Chapter 37's subject (pFUnit unit tests and regression tests against the analytical solution). They are complements: an assertion checks an invariant on every run in production, where a test checks a specific case before you ship. A codebase wants both — assertions to catch the impossible in the field, tests to catch the wrong before it leaves.


13.6 The Usual Suspects

Every language has a rogues' gallery of characteristic bugs, and knowing Fortran's by name is half of never being caught by them. Here are the five the outline names, each shown as the mistake and then the fix.

1. Uninitialized variables. Fortran does not zero your local variables. A freshly declared local holds whatever bits were in that memory, and reading it before you assign it is undefined behavior — which is insidious precisely because the garbage is often zero by luck, so the bug hides on your machine and appears on the cluster. The fixes are layered: initialize explicitly before first use; compile with -Wall, which warns about many (not all) use-before-set cases; and, to catch the rest, compile with -finit-real=snan (Appendix C), which fills uninitialized reals with a signaling NaN so that using one trips -ffpe-trap=invalid at the exact line — turning a silent bug into §13.3's loud one.

2. Out-of-bounds access. Indexing a(0) or a(n+1), often from an off-by-one in a loop or from forgetting that Fortran arrays are 1-based (Chapter 5). Silent by default; caught immediately by -fcheck=bounds (included in -fcheck=all), as §13.3 showed. This is the single best reason to keep -fcheck=all on during development.

3. Integer overflow. A default integer (32-bit) holds values up to huge(0_int32) = 2,147,483,647. Exceed it and it wraps silently to a negative number. The classic scientific trap is a product of grid dimensions — nx*ny*nz for a large 3-D grid, or a total element count — that quietly exceeds two billion. Watch it happen, and watch a 64-bit integer fix it:

program overflow_and_save
  use, intrinsic :: iso_fortran_env, only: int32, int64
  implicit none
  integer(int32) :: big32
  integer(int64) :: big64
  integer :: k

  do k = 1, 3;  call buggy_counter();  end do   ! implicit-save trap (suspect 5)
  do k = 1, 3;  call good_counter();   end do

  big32 = huge(0_int32)
  print '(a, i0)', 'huge(int32)      = ', big32
  big32 = big32 + 1_int32                        ! signed overflow: wraps
  print '(a, i0)', 'huge + 1 (int32) = ', big32
  big64 = int(huge(0_int32), int64) + 1_int64    ! 64-bit: no overflow
  print '(a, i0)', 'huge + 1 (int64) = ', big64

contains

  subroutine buggy_counter()
    integer :: count = 0    ! initializer in the declaration => IMPLICIT SAVE
    count = count + 1
    print '(a, i0)', 'buggy count = ', count
  end subroutine buggy_counter

  subroutine good_counter()
    integer :: count        ! no initializer: a fresh local each call...
    count = 0               ! ...reset explicitly on entry
    count = count + 1
    print '(a, i0)', 'good count = ', count
  end subroutine good_counter

end program overflow_and_save
$ gfortran -std=f2018 -Wall -O2 example-03-usual-suspects.f90 -o suspects && ./suspects
buggy count = 1
buggy count = 2
buggy count = 3
good count = 1
good count = 1
good count = 1
huge(int32)      = 2147483647
huge + 1 (int32) = -2147483648
huge + 1 (int64) = 2147483648

The integer arithmetic is exact and hand-checkable: huge(0_int32) is 2,147,483,647; adding one to a 32-bit signed integer overflows and wraps to $-2{,}147{,}483{,}648$ (gfortran uses two's-complement wraparound); the same computation in a 64-bit integer gives the correct 2,147,483,648. The fix in real code is to size counts and index products as integer(int64) whenever they might exceed two billion. (A note on standards correctness: signed integer overflow is not defined by the standard — gfortran wraps, but a program should not rely on it. You can make overflow trap instead with -fsanitize=undefined or -ftrapv; we show the wrap here only to make the danger visible.)

4. Precision loss. Reals are approximate (Chapter 20), and three habits go wrong: comparing them with == (if (x == 0.3_dp) is almost never what you want — use abs(x - 0.3_dp) < tol); mixing single and double precision so a real(dp) computation is silently degraded by a single-precision literal (always write the kind suffix, 1.0_dp); and the integer-division trap you met in Chapter 3, where 1/2 is 0 because both operands are integers. The unifying fix is the discipline this book has taught since Chapter 3: one precision kind everywhere (real(dp)), kind-suffixed literals, and tolerance comparisons for reals.

5. Implicit save. This is the trap that catches the reader off guard, and the program above shows it. When you give a local variable an initializer in its declarationinteger :: count = 0 — Fortran does not re-initialize it on each call. Instead the variable is implicitly given the save attribute: it is initialized once, before the program starts, and retains its value between calls. So buggy_counter prints 1, 2, 3 across three calls, not the 1, 1, 1 a newcomer expects. The good_counter version separates declaration from assignment — integer :: count then count = 0 — so count is reset on every entry and prints 1, 1, 1. The rule: an initializer in a declaration means "save," not "reset." If you want a fresh value each call, assign it in an executable statement; if you genuinely want persistence, write save explicitly to say so on purpose.

🐛 Find the Bug. A colleague writes an accumulator subroutine and cannot understand why the running total keeps growing across calls that should each start fresh:

fortran subroutine accumulate(x) real(dp), intent(in) :: x real(dp) :: total = 0.0_dp ! "start at zero every call" total = total + x print *, 'total = ', total end subroutine accumulate

Diagnosis The initializer = 0.0_dp in the declaration makes total implicitly saved: it is set to zero once, before the program runs, and thereafter keeps its value between calls, so the total accumulates across every call — the opposite of the comment's intent. The fix is to separate the declaration from the reset: real(dp) :: total on one line and total = 0.0_dp as the first executable statement. If persistence were desired, the honest spelling is an explicit real(dp), save :: total = 0.0_dp, so the intent is stated, not stumbled into.

🔧 Modern vs Legacy: implicit save is a special hazard when you modernize old code (Part IV). FORTRAN 77 code frequently relied on locals retaining their values between calls — sometimes on purpose, often by accident of how a particular compiler laid out static memory. When you convert such a routine, a DATA-initialized or saved local that you "clean up" into a modern initializer keeps its save semantics, while one you rewrite to assign on entry loses them — and if the old code secretly depended on persistence, your tidy modern version changes the answer. The lesson of the "legacy code is not a burden" theme applies precisely here: understand why the old code held state before you change how it does. When in doubt, make save explicit so the intent is on the page.

🔄 Check Your Understanding. 1. Why can an uninitialized-variable bug pass all your tests on your laptop and then fail on a cluster? 2. integer :: n = 0 inside a subroutine — is n reset to 0 on every call? What attribute does the initializer silently confer? 3. A grid is $2000 \times 2000 \times 2000$. Why might nx*ny*nz computed as default integer give a negative number, and what is the fix?

Answers 1. An uninitialized local holds whatever was in that memory; it is often zero by luck on one machine and something else on another, so the bug is platform-dependent. -finit-real=snan plus -ffpe-trap=invalid makes it fail loudly everywhere. 2. No — it is not reset each call. The initializer confers an implicit save, so n is initialized once and keeps its value between calls. Assign n = 0 in an executable statement for per-call reset. 3. $2000^3 = 8\times10^9$ exceeds the 32-bit signed limit of about $2.1\times10^9$, so the product overflows and wraps negative. Compute it in integer(int64) (e.g. size the operands as 64-bit).


Project Checkpoint

Your solver reads a namelist config (Chapter 7) and holds its state in a field_t derived type (Chapter 9). It is also, right now, credulous: hand it nx = 0, a negative dt, or a grid too large for memory, and it will either crash cryptically or — worse — allocate something nonsensical and march forward. This checkpoint makes it robust. We add a validate_config routine that checks the configuration before any memory is touched, we guard the field allocation with stat, and we error stop with a clear message and a distinct exit code on each kind of failure.

The heart of the increment is the validator — every precondition the run depends on, checked at the boundary:

subroutine validate_config(nx, ny, dt, alpha)
  integer,  intent(in) :: nx, ny
  real(dp), intent(in) :: dt, alpha
  ! Preconditions: a physical run needs a positive grid, timestep, and diffusivity.
  if (nx < 1 .or. ny < 1) then
     write(error_unit, '(a, i0, a, i0)') 'config error: grid must be positive, got ', nx, ' x ', ny
     error stop 2
  end if
  if (dt <= 0.0_dp) then
     write(error_unit, '(a, es10.2)') 'config error: dt must be positive, got ', dt
     error stop 2
  end if
  if (alpha <= 0.0_dp) then
     write(error_unit, '(a, es10.2)') 'config error: alpha must be positive, got ', alpha
     error stop 2
  end if
end subroutine validate_config

The driver then validates, allocates the field guarded, and only afterward sets up the plate. With a valid configuration (nx = 5, ny = 4, dt = 0.25, alpha = 1.0e-4) it runs cleanly:

$ gfortran -std=f2018 -Wall -fcheck=all -g project-checkpoint.f90 -o checkpoint && ./checkpoint
grid validated : 5 x 4
sum(u)         : 400.00
ready to step (Chapter 24).

The field is $5 \times 4$; we set the hot top row u(1,:) = 100 across all four columns, so sum(u) = 4 \times 100 = 400.00, everything else being zero. The full, self-contained program — bundling a small kinds module so it builds alone — is code/project-checkpoint.f90, with the hand-computed output.

Three decisions carry the chapter's lessons. Validate before you allocate: a bad grid is caught before a single byte is requested, and the message names the offending value. Distinct exit codes: configuration failures error stop 2, an allocation failure error stop 3, so a wrapper script can tell "you gave me nonsense" apart from "the machine ran out of memory" by reading $? alone. And build with -fcheck=all -g while developing (note the flag in the compile line above), so an off-by-one in the stencil you write in Chapter 24 is a runtime error with a line number, not silent corruption — then strip -fcheck for the production runs of Chapter 30. When the validator folds into the heat_types module, field_init's own allocate gains a stat= check by the same pattern. There is one more precondition worth adding as the solver becomes real: the five-point stencil needs an interior, so a robust validate_config will eventually require nx >= 3 .and. ny >= 3, and — once you meet the CFL condition in Chapter 24 — will check that dt is small enough that the explicit scheme does not blow up into the very NaNs that -ffpe-trap would otherwise catch for you. Robustness is not a one-time step; it is a habit the rest of the book keeps building.


Summary

This chapter turned optimistic code into robust code: code that asks whether each operation worked, halts deliberately when it cannot go on, and checks its own beliefs.

Concept The short version
stat / errmsg The iostat idea for memory: allocate(a(n), stat=s, errmsg=msg). s = 0 on success, nonzero (processor-dependent) on failure. Guard every allocate.
error stop vs stop stop = normal end, exit code 0. error stop [N] = error end, nonzero exit code (chosen N), halts all images in parallel. Report failure so the shell/CI notices.
Exit codes 0 = success, nonzero = failure; taken mod 256 on Unix. Use distinct small codes per failure category; read with echo $?.
-fcheck=all Runtime checks (bounds, allocation, pointers). Catches out-of-bounds with a line number. Development only — remove for production.
-fbacktrace Print a call-stack trace on a crash. Pair with -g. Cheap; keep it on.
-ffpe-trap=invalid,zero,overflow Halt at the operation that makes a NaN/Inf, instead of letting it propagate silently. Never trap underflow/inexact.
gdb / valgrind gdb: breakpoint + print/next/step/bt to see why a value is wrong. valgrind: find leaks, dangling reads, out-of-bounds — slow, small cases.
assertion A hand-written assert(cond, msg) that error stops if cond is false. An executable belief for the "impossible," distinct from stat/iostat for the expected.
defensive programming Validate inputs (preconditions), check results (postconditions), guard every failure point, initialize everything, report to error_unit with context. Fail early and loud.
The usual suspects Uninitialized variables; out-of-bounds (-fcheck); integer overflow (use int64); precision loss (== on reals; kind suffixes; integer division); implicit save (an initializer in a declaration means "save," not "reset").

The two things to memorize. First, stat/errmsg is iostat/iomsg for memory — guard every allocate, test the integer, show the message, and error stop with a chosen code so failure is visible to the outside world. Second, an initializer in a declaration confers an implicit saveinteger :: n = 0 inside a procedure is set once and persists, not reset per call; assign in an executable statement when you want a fresh value.

The heat-solver piece added this chapter: validate_config, checking nx, ny, dt, and alpha at the boundary; a stat-guarded field allocation; distinct error stop codes for config vs allocation failure; and the habit of building with -fcheck=all -g while developing.

Spaced Review

Retrieval practice on the two chapters this one generalizes and builds on — I/O error handling (Chapter 7) and pointers (Chapter 11). Answer before peeking.

  1. (Ch. 7) After a read, what do iostat values of zero, a negative number, and a positive number each mean — and how is this the same shape as the stat you met in §13.1?

    Answer Zero = success; negative = end-of-file (or end-of-record); positive = an error. `stat` on `allocate` uses the same pattern — zero for success, nonzero for failure — because both are the one Fortran idea: an operation that can fail returns a status you test, rather than throwing. (There is no end-of-file analogue for `allocate`, so `stat` is simply zero/nonzero.)

  2. (Ch. 7) Why should an end-of-file test compare iostat against the named constant iostat_end rather than the literal -1, and how does the same reasoning apply to the value in stat?

    Answer The exact value the runtime assigns at end-of-file is processor-dependent (commonly `-1`, but not guaranteed), so portable code uses `iostat_end` from `iso_fortran_env`. The same reasoning forbids testing `stat` against a magic number like `151`: the nonzero failure value is processor-dependent, so the only portable test is `stat /= 0`.

  3. (Ch. 11) A pointer q still aliases memory that was freed through another pointer p. Why does this dangling pointer defeat ordinary testing, and which of this chapter's tools would catch a read through it?

    Answer After `deallocate(p)`, `q` still holds the old address and may even report `associated(q) == .true.`, so no in-language test detects it — it is undefined behavior that "passes." valgrind (§13.4) catches the invalid read of freed memory at the offending line; `-fsanitize=address` can too. The language-level fix from Chapter 11 is discipline: `nullify` every alias after `deallocate`.

  4. (Ch. 11) The solver's field_t stores its grid as an allocatable component, not a pointer component. Give two ways this choice removes an entire class of the bugs §13.4's valgrind hunts for.

    Answer A plain `allocatable` is freed automatically when it goes out of scope, so it cannot **leak** (no forgotten `deallocate` on an error path); and it can never be aliased, so it cannot **dangle** (no second pointer to be left pointing at freed memory). Two whole valgrind categories — definite leaks and dangling reads — become impossible, which is half of Chapter 11's "prefer `allocatable`" argument.

What's Next

Part II is complete. Your solver is now organized into modules, its state lives in a purpose-built type, and — as of this chapter — it validates its inputs and fails loudly and clearly when something is wrong. That is code an engineer would be willing to maintain, and it is exactly the code you want before you connect it to the wider world. Chapter 14 opens Part III by teaching Fortran to talk to C — the iso_c_binding module, the bind(c) attribute, and the art of passing arrays and structs across the language boundary. Error handling comes with you: C reports failure through return codes and the global errno, not through stat or error stop, so calling between the languages means translating between two error conventions. The robustness discipline you built here is what lets you do that translation without losing the thread of what went wrong. Next, we cross the border.