Exercises: Error Handling, Debugging, and Defensive Programming

These exercises train the habit the chapter is really about: writing code that tells you when it is wrong. Some ask you to reason, some to predict output, several to hunt bugs, and a few to harden the heat solver. Do the "predict first, then compile" problems honestly — the value is in the gap between what you expected and what the compiler actually did.

Difficulty: ⭐ warm-up · ⭐⭐ standard · ⭐⭐⭐ deeper. Solutions: worked solutions to the daggered (†) and odd-numbered problems are in appendices/answers-to-selected.md; the computational ones also appear as compilable code in code/exercise-solutions.f90. Try every problem before you look.

A safety note repeated from the chapter: several problems below describe programs that intentionally misbehave (crash, wrap, dangle). Reason about what they would do; where a crash message is shown, treat it as representative and version-dependent. You will learn more by predicting the outcome than by rushing to run it.


Part A — Warm-ups ⭐

13.1 † The chapter calls stat/errmsg "the iostat mechanism from Chapter 7, generalized." Generalized to what, and what is the one-sentence pattern the two share?

13.2 Give two concrete differences between stop and error stop.

13.3 † Name two distinct classes of bug that -fcheck=all catches at run time that a plain compile does not.

13.4 What is an assertion, and how does its job differ from that of iostat/stat error handling? (Hint: expected conditions vs "impossible" ones.)

13.5 † Define defensive programming in one sentence, and name two of its concrete practices from §13.5.


Part B — Type, Compile, and Run ⭐⭐

Predict the output before you compile. Then run it and reconcile.

13.6 A subroutine contains integer :: hits = 0, then hits = hits + 1, then prints hits. It is called three times in a loop. What does it print each time, and why is it (probably) not what a newcomer expects?

13.7 † Predict the two lines printed by:

integer(int32) :: n
n = huge(0_int32);   print '(i0)', n
n = n + 1_int32;     print '(i0)', n

State both values exactly, and name the phenomenon.

13.8 A program ends with error stop 5. After running it in a shell, what does echo $? print, and what would it print if the program had ended with a bare stop instead?

13.9 † This program is compiled with gfortran -std=f2018 -fcheck=all -g:

integer :: a(5), i
a = 0;  i = 6;  a(i) = 99;  print *, a

Describe, in shape, what the run reports and at which line — and what the same program would most likely do compiled without -fcheck.


Part C — Find the Bug ⭐⭐

Each snippet is wrong. Diagnose it and give the fix.

13.10 † "It aborts on the big grid instead of falling back."

allocate(u(nx, ny))          ! nx, ny come from user input
u = 0.0_dp

13.11 "associated(q) says true, but reading q crashes."

real(dp), pointer :: p => null(), q => null()
allocate(p);  q => p;  deallocate(p)
print *, associated(q), q

13.12 † "The running total from this routine grows across calls that should each start at zero."

subroutine running_sum(x)
  real(dp), intent(in) :: x
  real(dp) :: total = 0.0_dp
  total = total + x
  print *, total
end subroutine

13.13 "The convergence test never fires even though the values look equal."

if (residual == 1.0e-6_dp) then
   print *, 'converged'
end if

13.14 † "Halving the step size gives zero."

integer :: n = 5
real(dp) :: dt
dt = 1 / n / 2          ! meant to be (1/n)/2 as a real

Part D — Port It ⭐⭐

Translate the idea to modern Fortran and compare the error models.

13.15 † Port this Python to Fortran, preserving the behavior (report a helpful message, then exit with a nonzero code) using iostat/iomsg and error stop. Note in a sentence how Fortran's model differs from Python's try/except.

try:
    f = open("config.nml")
except OSError as e:
    print("cannot open config:", e)
    sys.exit(2)

13.16 Port assert n > 0, "n must be positive" (Python's assert) to a Fortran call using the assert subroutine from §13.5. What is the Fortran equivalent of running Python with -O to strip asserts?


Part E — Design It (the Heat Solver) ⭐⭐

13.17 † Extend validate_config (Project Checkpoint) so it also rejects a grid with no interior — the five-point stencil of Chapter 24 needs nx >= 3 .and. ny >= 3. Write the check and a message that names the offending size, ending in error stop 2.

13.18 After a time step, add a postcondition that the field contains no NaN. Write a logical function has_nan(u) (hint: a NaN is the only value not equal to itself — x /= x), and call assert(.not. has_nan(field%u), 'step produced NaN'). Why is x /= x a valid NaN test?

13.19 † Give the solver three distinct exit codes — say 2 for bad config, 3 for allocation failure, 4 for a NaN blow-up — and sketch (in shell, 4–6 lines) a wrapper that runs the solver and prints a different message for each code by reading $?.


Part F — Back of the Envelope ⭐⭐⭐

Order-of-magnitude reasoning; show your work.

13.20 † A 3-D solver stores its grid as a single flattened array and indexes it with a default 32-bit integer. Above roughly what cubic side length $L$ (for an $L\times L\times L$ grid) does the total cell count L**3 overflow a 32-bit signed integer? For $L = 1500$, compute the wrapped value exactly.

13.21 Bounds checking adds a comparison to every array access. If a stencil kernel does ~5 array reads and 1 write per cell and you run $10^8$ cells for $10^4$ steps, roughly how many extra bounds comparisons does -fcheck=bounds introduce over the whole run? Argue from this why it is a development-only flag.

13.22 † A NaN first appears at step 30,000 of a 100,000-step run and you notice only in the final output. Estimate, in orders of magnitude, how many arithmetic operations separate the cause from the symptom, and explain how -ffpe-trap=invalid -fbacktrace collapses that distance to a single line.


Part G — Interleaved ⭐⭐

Mixing this chapter with earlier ones.

13.23 † (Ch. 7) Write the guarded read of a namelist group /config/ from heat.nml: open with iostat/iomsg, error stop with the iomsg text if the open fails, then read(u, nml=config, iostat=ios) and report a misspelled key failure distinctly from a missing file failure.

13.24 (Ch. 11) Explain, in two sentences, why the solver's allocatable field can never produce the two valgrind errors "definitely lost" (leak) and "invalid read of freed memory" (dangling), while a pointer field could.

13.25 † (Ch. 9) For two field_t values, b = a is a deep copy. Which aliasing bug class does that value semantics make impossible, and how would a pointer component have reintroduced it?

13.26 (Ch. 5) You sweep a field u(nx,ny) with the inner loop over j (the second index) and get a -fcheck=bounds error only on the last iteration. Which index is out of range, and how does column-major order (Chapter 5) also make this loop slow even once you fix the bound?

13.27 † (Ch. 3) A colleague reports "precision loss": frac = 3 / 4 gives 0.0. Is this really a floating-point precision problem? Diagnose it and give the fix.

13.28 (Ch. 2) List the flags you would use for a development build of the solver and the flags for a production build, and name the one flag that must move from the first set out of the second.


Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the computational solutions (13.7, 13.15, 13.20) are also in code/exercise-solutions.f90. Remember: for any problem that describes a crashing or wrapping program, the expected diagnostic is representative and version-dependent — reason about the behavior, do not trust a pasted message.