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.