Thinking ahead: preconditions, caching and reuse

A good cook doesn't start frying and then wonder whether they've chopped the onions. They read the whole recipe first: they line up the ingredients, get the oven hot, and set aside the sauce they made yesterday so they don't have to make it again. Long before any real work happens, they've already thought about what each step needs and what they can prepare or reuse.

This is a genuine computational-thinking skill, and it is exactly how experienced programmers work. Before writing a line of code, they ask two forward-looking questions:

Neither question is about the step in front of you. Both are about planning ahead — and getting them right is often the difference between a solution that is correct and fast and one that is buggy and slow. This page is about learning to think that way on purpose.

Preconditions: what must be true first

A precondition is a condition that must hold before a step is allowed to run, or the step will fail or produce a wrong answer. Almost every operation you can think of quietly assumes one:

Binary search is the classic exam example. It halves the search space each step by comparing with the middle element and deciding which half the target must be in. That reasoning is only valid if the list is sorted; on an unsorted list it will confidently throw away the half that actually contains the answer and report "not found". The sortedness isn't a nice-to-have — it is the precondition that makes the whole method correct.

Thinking ahead means spotting these before you run the step, and arranging for them to be true: sort the list first, check the file exists, guard against a zero divisor. A step whose preconditions you've secured is a step you can trust.

A precondition is a statement that must be true immediately before an operation runs for that operation to behave correctly.

Seeing a precondition matter

Let's watch binary search on a sorted list, then on the same numbers left unsorted. The code is identical; only the precondition changes. Press Run.

// Binary search ASSUMES its input list is sorted ascending. // That assumption is its precondition. function binarySearch(list: number[], target: number): number { let lo = 0, hi = list.length - 1; while (lo <= hi) { const mid = Math.floor((lo + hi) / 2); if (list[mid] === target) return mid; // found it if (list[mid] < target) lo = mid + 1; // must be in the RIGHT half... else hi = mid - 1; // ...or the LEFT half } return -1; // not found } const sorted = [2, 5, 9, 13, 21, 34, 55]; // precondition MET const jumbled = [21, 2, 55, 9, 34, 13, 5]; // same numbers, precondition BROKEN console.log("Looking for 34 in the SORTED list:"); console.log(" found at index", binarySearch(sorted, 34), "(correct)"); console.log("Looking for 34 in the JUMBLED list:"); console.log(" found at index", binarySearch(jumbled, 34), "(-1 means 'not found' — but 34 IS there!)");

The second search fails silently. It doesn't crash; it just lies — it says 34 isn't there when it plainly is. That's the danger of an unmet precondition: the code runs, so nothing looks wrong, but the answer is garbage. Thinking ahead, we'd sort the list before searching it, restoring the precondition and making the result trustworthy again.

Caching and reuse: do the hard work once

The second half of thinking ahead is refusing to repeat expensive work. If a result is costly to compute and you'll need it again, compute it once, store it, and look it up next time. That stored copy is a cache, and the trade is always the same: you spend a little memory to save a lot of time.

You already rely on caches all day long:

Here is a precomputed lookup table beating recomputation. Imagine we repeatedly need factorials. Working one out means a loop of multiplications; if we know in advance we'll only ever ask about the numbers 0–12, we can precompute all of them once into a table, then answer every future request with an instant array lookup. A counter proves how much work each approach does.

// --- recompute every time: a fresh loop for each request --- let workDone = 0; function factorialSlow(n: number): number { let result = 1; for (let i = 2; i <= n; i++) { result *= i; workDone++; } // real work each call return result; } // --- precomputed lookup table: build ONCE, then just look up --- let buildWork = 0; const table: number[] = [1]; // 0! = 1 for (let i = 1; i <= 12; i++) { table[i] = table[i - 1] * i; buildWork++; } function factorialFast(n: number): number { return table[n]; } // instant — no loop // Pretend a program asks for these factorials over and over. const requests = [5, 8, 5, 12, 8, 5, 10, 12, 8, 5]; for (const n of requests) factorialSlow(n); for (const n of requests) factorialFast(n); console.log("Recompute-every-time did", workDone, "multiplications for", requests.length, "requests."); console.log("Precomputed table did", buildWork, "multiplications ONCE, then 0 work per request."); console.log("5! is", factorialFast(5), "either way — same answer, far less work.");

The table pays a small fixed cost up front and then answers every request for free. The more times the answers are reused, the more the precomputation pays off — which is the whole point of thinking ahead: the up-front effort is an investment against work you can see coming.

A game might rotate hundreds of objects sixty times a second. Working out Math.sin() from scratch that often adds up. So many games build a lookup table once when they start — the sine of every whole-degree angle, say — and thereafter just read the answer straight out of the array. It's the same bargain as the factorial table: a one-off precomputation buys instant answers forever after. Old hardware relied on this trick so heavily that lookup tables were practically a programming style.

Reuse is planning, not just speed

Reuse isn't only about caching values — it's also about reusing code. When you notice two steps need the same calculation, thinking ahead means writing it once as a subroutine and calling it from both places, rather than copying the logic. You get the work, and the testing of that work, for free the second time.

// Written and tested ONCE... function average(scores: number[]): number { let total = 0; for (const s of scores) total += s; return total / scores.length; } // ...then reused wherever an average is needed — no copy-paste, no re-testing. const mathsMarks = [55, 70, 85, 90]; const scienceMarks = [60, 72, 66]; console.log("Maths average: ", average(mathsMarks)); console.log("Science average:", average(scienceMarks)); console.log("Overall of the two subject averages:", average([average(mathsMarks), average(scienceMarks)]));

Notice the last line reuses average() to average two averages. Because the subroutine already exists and is trusted, building bigger things out of it costs almost nothing. That's reuse as a design habit: solve a piece well once, then lean on it.

Thinking ahead has two failure modes, and both are favourites in exams.

The common thread: both are the price of planning ahead. A cache and a precondition are each a promise about the world — "this stored answer is still current", "this list really is sorted" — and a promise you don't keep is worse than one you never made.