← Fundamentals
Guided lesson 5 questions · 6 languages

Cleanup on a failing path

Take, use, release: on the way out that works, and on every way out that fails.

A function that takes a resource must release it on every way out: the last line, each return, and each failure. The failing path is the one that is easy to forget, so let the language run the release for you.

Before you begin

Why this matters

A release that is skipped causes no error message. The program goes on, and the thing it kept is missing later. Knowing which ways out skip a line of code lets you put the release where every way out runs it.

What this helps you reason about

  • Predict whether a release runs after a return or a throw.
  • Write try/finally so that the release runs on every path, and only after the take succeeded.
  • Spot a finally block that hides a failure.

A hospital entrance lends wheelchairs. Each visit takes one and gives it back. If a visit fails halfway, does the wheelchair still come back? This lesson follows the release through the path that works and through the paths that fail.

Already know this? The short version
  • A release written as an ordinary line is skipped by every return and every throw before it; tests of the successful path do not notice.
  • Take the resource, then open try; put the release in finally. finally runs after the last line, after a return (whose value is computed first), and after a throw.
  • For two resources, nest two try/finally blocks, each opened just after its take; release then happens in reverse order, and a take that throws releases nothing of its own.
  • Never return or throw from finally: a return there replaces the exception that was on its way out. using (TypeScript 5.2) disposes in reverse order, but the stepper’s library settings do not compile it.

Go to the final check· Go to the practice tickets

Words in this lesson (5)
resource
Something a program takes for a while and must give back, such as a booking, a lock, or an open file. While one part of the program holds it, other parts cannot have it.
cleanup
The code that releases what a function took, such as giving back a booking or closing a file. It must run on every way out of the function.
early return
A return that ends the call before the function’s last line, usually inside an if. The lines after it do not run in that call.
exception
A value that is thrown to stop the normal flow when an operation fails. In TypeScript, any value can be thrown, but code usually throws an Error object, which has a message.
finally
A block after try that runs whenever the try block ends: after its last line, after a return, or after a throw. After a throw, the exception continues to the caller once the finally block has run.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Early exits

What happens to a resource when a function exits early?

A resource is something a program takes for a while and must give back: a booking, a lock, an open file. In this lesson it is a wheelchair at a hospital entrance. The program counts the wheelchairs in use. visit takes one and must release it before it ends. desk.inUse is the count.

The release is an ordinary line near the end of visit. A line runs only when the program reaches it. The return inside the if leaves early, so a refused visit keeps its wheelchair. A throw has the same effect: when a call in the middle throws an exception, the lines after it in this function do not run.

The failing path is the one people forget. A test that tries only visits that succeed shows nothing wrong. The count grows slowly, one refused visit at a time, until no wheelchair is left.

main.ts
function visit(desk: Desk, ward: string): string {
  desk.inUse = desk.inUse + 1; // take a wheelchair
  if (ward === "closed") {
    return "refused"; // the release below is skipped
  }
  desk.inUse = desk.inUse - 1; // release it
  return "done";
}

A release written as an ordinary line runs only on the paths that reach it.

Coming from Python: TypeScript has no with statement; try/finally does the same work by hand.

Experiment

Follow a refused visit past its release

A line runs only when the program reaches it. return ends the call at once, so the line that releases the wheelchair is skipped on that path.

Why it matters Check every return, and every call that can throw, between taking a resource and releasing it.

Predict both printed lines. Then add desk.inUse = desk.inUse - 1; above return "refused"; and predict again.

type Desk = { inUse: number };

function visit(desk: Desk, ward: string): string {
  desk.inUse = desk.inUse + 1; // take a wheelchair
  if (ward === "closed") {
    return "refused";
  }
  desk.inUse = desk.inUse - 1; // release it
  return "done";
}

const desk = { inUse: 0 };
console.log(visit(desk, "B2"), desk.inUse);
console.log(visit(desk, "closed"), desk.inUse);
What to look for

Step into the second call. desk shows {inUse: 1} after the first line of visit. The if is true, the next stop is the return line, and the release line is never highlighted. The run prints done 0, then refused 1. After the edit, the run prints refused 0. Node’s debugger can stop on one line more than once; that is normal.

Expected output

done 0
refused 1

02 / Every exit

How do you release on every way out?

A finally block runs whenever its try block ends: after the last line, after a return, or after a throw. Propagation and unwinding showed a finally block running while an exception passes through. That makes finally the place for a release.

The pattern has three parts. Take the resource. Open try at once, and put all the code that uses the resource inside it. Put the release in finally.

On the failing path, the order is: the throw, then the finally block, then the caller’s catch. On the successful path, the return value is computed first, then the finally block runs, then the caller receives the value. So after the experiment’s edit below, release is printed before done. A catch with no name in brackets, as in catch { … }, is allowed when the code does not need the thrown value.

main.ts
desk.inUse = desk.inUse + 1; // take
try {
  if (ward === "closed") {
    throw new Error("ward closed");
  }
  return "done";
} finally {
  desk.inUse = desk.inUse - 1; // release, on every path
}

Take the resource, then put everything that uses it in try and the release in finally.

Precisely: using, and the end of the program

TypeScript 5.2 added using declarations: using chair = …; calls chair[Symbol.dispose]() when the block ends, by any path, and several using declarations are disposed in reverse order. The stepper compiles with the ES2022 library, which lacks the types this needs, so it reports Cannot find global type 'Disposable'. process.exit ends the program at once, and finally blocks that have not run yet never run.

Experiment

Watch finally run on the failing path

finally runs whenever the try block ends. A throw first runs the finally block, then continues to the nearest catch. A return computes its value, runs the finally block, and then gives the value to the caller.

Why it matters Code in finally runs on the failing path and on the successful one, so a release there cannot be skipped.

Predict the three lines and their order. Then change visit(desk, "closed") to visit(desk, "B2") and predict again.

type Desk = { inUse: number };

function visit(desk: Desk, ward: string): string {
  desk.inUse = desk.inUse + 1;
  console.log("take, in use:", desk.inUse);
  try {
    if (ward === "closed") {
      throw new Error("ward closed");
    }
    return "done";
  } finally {
    desk.inUse = desk.inUse - 1;
    console.log("release, in use:", desk.inUse);
  }
}

const desk = { inUse: 0 };
try {
  console.log(visit(desk, "closed"));
} catch {
  console.log("refused, in use:", desk.inUse);
}
What to look for

Step to the throw line. The next stop is in the finally block, not in the caller: the release runs while visit is still on the frame list. Then visit leaves the frame list, and the catch block prints. The run prints take, in use: 1, then release, in use: 0, then refused, in use: 0. After the edit, the stop after the return line is again in the finally block, and done is printed last, after the release.

Expected output

take, in use: 1
release, in use: 0
refused, in use: 0

03 / Two resources

What changes when a function holds two resources?

Take each resource just before the try that releases it. Two resources then need two try blocks, one inside the other. The inner one releases what was taken last, so the release happens in the reverse order of the taking.

The place of take matters. take("blanket", …) is outside its own try. If it throws, no blanket was taken, and its finally block never starts. The outer finally still runs, because the wheelchair was taken. If take were inside the try, its finally would release a blanket that nobody has.

main.ts
take("wheelchair", 3);
try {
  take("blanket", blankets);
  try {
    console.log("visit");
  } finally {
    console.log("release blanket");
  }
} finally {
  console.log("release wheelchair");
}

Release in reverse order, and only what was taken: put each take just before its try, not inside it.

Experiment

Run out of blankets

Each finally belongs to one try. take("blanket", …) is outside the inner try, so when it throws, the inner finally never starts. The outer try was entered after the wheelchair was taken, so its finally runs.

Why it matters Release in the reverse order of taking, and put each take just before the try that releases it.

Predict the five lines and their order. Then change visit(2) to visit(0) and predict again.

function take(item: string, left: number): void {
  if (left === 0) {
    throw new Error("no " + item + " left");
  }
  console.log("take", item);
}

function visit(blankets: number): void {
  take("wheelchair", 3);
  try {
    take("blanket", blankets);
    try {
      console.log("visit");
    } finally {
      console.log("release blanket");
    }
  } finally {
    console.log("release wheelchair");
  }
}

try {
  visit(2);
} catch {
  console.log("refused");
}
What to look for

Watch the output panel. The run prints take wheelchair, take blanket, visit, release blanket, release wheelchair. After the edit, the run prints take wheelchair, release wheelchair, refused. The inner try is never entered: the blanket was never taken, so it is not released. Every frame also lists take and visit as variables, shown as function take(item, left): that is how the panel shows a function, not a copy of it.

Expected output

take wheelchair
take blanket
visit
release blanket
release wheelchair

04 / Cleanup traps

What can go wrong in the cleanup code itself?

Cleanup code can cause a failure of its own. A return inside finally replaces whatever the try block was doing. If the try block threw, the exception is dropped, and the caller receives the finally block’s value as if nothing had failed. A throw inside finally also replaces the first exception, so the caller sees the wrong failure.

Keep finally short: release, and nothing that returns or can throw.

main.ts
try {
  throw new Error("ward closed");
} finally {
  desk.inUse = desk.inUse - 1;
  return "released"; // the exception is lost
}

A return in finally hides the failure; keep finally for releasing.

Experiment

Return from finally after a throw

A return in finally replaces whatever the try block was doing, including a throw that is on its way out. The function ends normally with the finally block’s value.

Why it matters Never return from finally. Keep it for releasing.

Predict the printed line. Then delete the line return "released"; and predict again.

type Desk = { inUse: number };

function visit(desk: Desk, ward: string): string {
  desk.inUse = desk.inUse + 1;
  try {
    if (ward === "closed") {
      throw new Error("ward closed");
    }
    return "done";
  } finally {
    desk.inUse = desk.inUse - 1;
    return "released";
  }
}

const desk = { inUse: 0 };
try {
  console.log(visit(desk, "closed"), desk.inUse);
} catch {
  console.log("refused", desk.inUse);
}
What to look for

Step from the throw line into the finally block, then to return "released". The next stop is in the caller, and the catch block is never highlighted. The run prints released 0: the wheelchair was released, but the failure has gone. After the edit, the exception reaches the catch block, and the run prints refused 0.

Expected output

released 0

05 / Try it

Can you predict when each release runs?

Follow the two calls one at a time, and decide where each release line runs. I/O, buffering, and external state, a later lesson, shows the same rule for text that waits in a buffer when a program ends.

main.ts
function carry(patient: string): string {
  console.log("take stretcher");
  try {
    if (patient === "") {
      throw new Error("no patient");
    }
    return "carried " + patient;
  } finally {
    console.log("release stretcher");
  }
}

try {
  console.log(carry("Ada"));
  console.log(carry(""));
  console.log("all carried");
} catch {
  console.log("stopped");
}

What does the program print, in order?

Reveal the reasoning

take stretcher, release stretcher, carried Ada, take stretcher, release stretcher, stopped. The first call computes "carried Ada", runs finally, and only then gives the value to console.log, so the release comes first. The second call throws; finally runs, and the exception skips all carried and reaches the catch.

Practice tickets

  1. CLEAN-101 · Refused transfers leave oxygen cylinders signed out → Free
  2. CLEAN-102 · The porter desk shows more free porters than are on shift → Sign in · Pro
  3. CLEAN-103 · Booked health checks leave their places free → Sign in · Pro

Keep exploring

Panics, traps, and undefined behavior →

Primary references