From Pseudocode to Code
The pseudocode had six endings on one screen. The code has a route, a validator, an ORM, an SDK and a catch block — and, if you are not careful, one ending. What survives the translation, what the framework adds, and how to keep the branches visible.
The situation, the reflex, and why it stalls
Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.
You have good pseudocode. How do you turn it into code without losing the decisions it made?
The checkout pseudocode was the best design work I have done: four endings, every branch decided, the timeout case named. Then I implemented it in the framework and a reviewer asked "where is the unknown state?" and I looked and it was not there. The SDK threw on timeout, my catch block returned a 500, and the decision I had made on paper had been silently made again, differently, by the code.
Translate line by line. Each pseudocode line becomes the framework call that does it, top to bottom, and when the framework does something differently — throws instead of returning, validates in a decorator instead of a branch — follow the framework, because it knows its own idiom.
Following the framework's idiom moves the branches somewhere the pseudocode did not have. A return becomes an exception; the exception is caught by a handler three files away; the handler maps every exception to the same response. Six endings became two, and the four that vanished were the ones that had taken the most thought.
- Following the framework's idiom moves the branches somewhere the pseudocode did not have. A return becomes an exception; the exception is caught by a handler three files away; the handler maps every exception to the same response. Six endings became two, and the four that vanished were the ones that had taken the most thought.
- The line-by-line translation reproduces the pseudocode's order but not its decisions, because some decisions were in the structure — the order row created before the charge — and the framework's "save at the end" idiom quietly reordered it.
- The pseudocode is now a document that describes what the code was going to do. Nobody updates it, the code drifts, and the next person reads the code, sees two endings, and concludes that checkout has two endings.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Before translating, list what the pseudocode decided that must survive: the endings, the order of steps around the boundary, the state each failure leaves, the identity that makes a repeat safe. This is the checklist for the translation, and it is short — usually under ten items — because it is only the decisions, not the lines (Inputs, Outputs, State, Branches).
- Translate the endings first, as values or types the code returns, before translating any step. If the language has sum types or result types, each ending is one case; if it does not, each ending is an explicit return with a named status. The point is that the code has a place for every ending before it has a way to reach any of them, so the framework cannot collapse them (States That Must Be Unrepresentable).
- Then translate the steps, and at each boundary call decide explicitly what the framework's failure becomes: an SDK timeout becomes the unknown ending, not an escape into a catch block. Where the framework wants an exception, catch it at the call and convert it to the ending the pseudocode chose — the conversion is the line that keeps the decision (External Systems Fail).
- Accept what the framework genuinely adds — types, transactions, timeouts, validation, serialisation — as engineering the pseudocode did not have to do, and check that none of it changed a decision. A transaction that spans the charge call, a validator that rejects before the price check, a default timeout of forever: each is an addition that can silently overrule the design (Framework Independence).
The translation, side by side
Left, the pseudocode's boundary branch. Right, the same branch in a typed language, with the framework's exception converted at the call to the ending the design chose. The comment naming the pseudocode line is not decoration; it is how the next reader finds the decision.
1type CheckoutResult =2 | { kind: 'confirmed'; order: Order }3 | { kind: 'rejected'; reason: RejectReason }4 | { kind: 'failed'; reason: string }5 | { kind: 'unknown'; order: Order } // the ending the catch block had swallowed6 7// pseudocode: "payment = provider.charge(total, attemptKey); if payment unknown → ..."8let payment: ChargeOutcome9try {10 payment = await provider.charge(order.total, { idempotencyKey: attemptKey, timeoutMs })11} catch (e) {12 if (isTimeout(e)) {13 await orders.markUnknown(order.id, { reference: attemptKey }) // reservation stays held14 return { kind: 'unknown', order }15 }16 throw e // anything else really is unexpected, and the generic handler is right17}The result type gives the code a place for the unknown ending before any step reaches it. The catch is around one call and converts one failure; everything else still goes to the generic handler, which is where genuinely unexpected errors belong.
What gets lost, and where
Each row is a decision the pseudocode made and the framework idiom that most often undoes it in translation. The response column is the line that keeps the decision. None of the responses is "avoid the framework"; every one is "use it, and convert at the boundary".
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| A boundary call throws | The unknown ending disappears into a catch-all handler | The framework reports failure by exception; the handler maps all exceptions to one response | Catch at the call, convert to the named ending, rethrow anything the pseudocode did not decide. |
| ORM "save at the end" idiom | The order row is written after the charge | Line-by-line translation kept the verbs and lost the structural decision | Put "pending row before charge" on the survival checklist; write the row first and test the crash-after-charge injection. |
| Validator decorator | Malformed input is rejected before the price check, with a different response shape | The framework added an ending the pseudocode did not have | Add the ending to the pseudocode and the result type; it is a real improvement, and now it is a decision. |
| SDK default timeout | Checkout hangs forever on a slow provider | The pseudocode assumed a timeout existed; the SDK's default was none | Set it explicitly; the value is a decision and belongs beside the unknown ending. |
| Retry helper around the SDK call | A timeout is retried and the card is charged twice | A resilience idiom applied to a call the pseudocode said not to retry | The "no retry" line is on the checklist; retries go around the status query, never the charge (Duplicate Requests). |
The slice that proves the translation
The translation is finished when one slice runs through every layer for the ending that was hardest to keep — not the happy path, which survives any translation, but the unknown ending, which is the one the catch block ate. The slice below is that: a hanging fake provider, a request, and the order's state observed in the database.
- BrowserSubmits checkout with the attempt key generated on the review page.
- APIValidates the body; reserves the attempt key; calls checkout.
- LogicWrites the pending order and reservation in one transaction; calls the provider with a timeout; converts the timeout to the unknown ending.
- DatabaseHolds the order in unknown with its reference; the reservation is still held.
- ResponseReturns the confirming-payment shape, not an error.
How to do it
Most important first.
- Write the survival checklist from the pseudocode: endings, boundary order, failure states, repeat identity. Pin it next to the editor.
- Define the endings in code first — a result type, an enum, a set of named returns — and make the function's signature say it returns one of them.
- Translate the happy path. Run the test for the confirmed ending.
- Translate each branch, converting framework exceptions at the boundary into the pseudocode's ending. Run the test for that ending before the next branch.
- Read the finished code as a stranger and count the endings. If the count is lower than the pseudocode's, find where the framework merged them (A Slice Is Testable).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Checkout's survival checklist: four endings (confirmed, rejected, failed, unknown); pending order row before the charge call; declined → failed with reason and released reservation; timeout → unknown with reference and held reservation; attempt key reserved before the charge; no retry around the charge call. Seven items, on a sticky note.
- The endings in code first: a result type with four cases, and a handler that maps each case to a status and a body — rejected and failed to a client-error response with the reason, unknown to a success-shaped response saying the payment is being confirmed, confirmed to the confirmation. The mapping is one function and it is the only place a status code appears.
- The boundary call: the SDK throws a timeout error; a catch around only that call converts it to the unknown ending with the reference stored, and a comment names the pseudocode line it implements. A test with the fake provider hanging asserts the order is in unknown and the response is the confirming-payment shape — the same test that existed as a leaf in the decomposition before any code did.
- What the framework added and was checked: the ORM's transaction wraps the order row and the reservation and nothing else — the charge call is outside it on purpose; the validator runs before the price check and rejects malformed input, which is a fifth ending the pseudocode did not have and which was added to it; the SDK's default timeout was set explicitly because the pseudocode had assumed one existed.
How you know it worked
What now exists that did not before, and what question you can now ask.
- The code returns a value with the same number of cases as the pseudocode has endings, and a test exists for each case.
- Every framework exception at a boundary is converted at the call site to a named ending, and the conversion carries a comment naming the design decision.
- The pseudocode's structural decisions — what comes before the charge, what is inside the transaction — can be pointed at in the code.
The questions you can now ask
The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.
- ?Which decisions in this pseudocode must survive the translation — endings, order at the boundary, failure states, identity?
- ?Does the code have a place for every ending before it has a way to reach any of them?
- ?At each boundary call, what does the framework's failure become, and where is the line that decides?
- ?What did the framework add, and did any of it change a decision the pseudocode made?
What can go wrong
- The code is written to look like the pseudocode — the same names, the same order, no framework idiom at all — and fights the framework everywhere, so that the next engineer, who knows the framework, cannot read it. The decisions survive; the idiom can change.
- The survival checklist becomes the whole design document, and the pseudocode is kept in sync with the code forever. Once the tests pin the endings, the pseudocode has done its job and can be a historical note.
- The framework's additions are refused on principle — no validator, no ORM transaction — because they were not in the pseudocode. They were not in it because pseudocode does not do engineering; the check is whether they changed a decision, not whether they exist.
- Explicit endings and per-call conversions are more code than one catch block, and to someone reading only the framework's idiom they look like reinvention.
- Defining the endings before the steps means nothing runs end to end until later than a line-by-line translation would; the walking skeleton is delayed by the type.
- "The code should mirror the pseudocode." The decisions should survive; the shape can and should change. Idiomatic code with four visible endings is the goal, not un-idiomatic code with the pseudocode's line breaks.
- "Exceptions are bad." Exceptions are how many frameworks report boundary failure, and that is fine. The mistake is letting one catch block turn several endings into one; catching at the call and converting keeps the decision.
- "The pseudocode was wrong because the validator found a fifth ending." The pseudocode was incomplete in a way the framework revealed, which is the framework adding value. Add the ending to the pseudocode and the tests; the design improved.
Where this applies
Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.
- GENERALThe survival checklist — endings, boundary order, failure states, identity — is what any translation from a design to a language must preserve; the mechanics of preserving it vary with the language's type system and the framework's error idiom.
- DOMAIN-SPECIFICFor an operation with lasting external effects the boundary conversions are the whole point and worth every line; for an internal read with one failure mode, a single catch block is honest and the checklist has one item.
- ILLUSTRATIVEThe reviewer, the sticky note with seven items and the four-case result type are invented to show the translation preserving decisions; a real framework's idioms will move the lines around differently.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — Build Without AI at /thinking/without-ai reviews your attempt against exactly this question: did the endings in your pseudocode survive into your code, or did the framework decide them again?