- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Reproducible Failure to Verified TypeScript Fix
GloboTicket's admin dashboard runs a nightly batch job that replays a queue of customer orders through a checkout ledger to compute each order's total. A support ticket reports that whenever more than one order runs in the same batch, every order after the first comes out overcharged by a growing amount. The calculation module builds each order's total with a running accumulator, and something is letting one order's numbers bleed into the next. Your job is to make the failure happen the same way every time, follow it from the compiled JavaScript back to the original TypeScript line, pause at the moment the accumulator goes wrong to see exactly what it holds, cut the framework's own call frames out of the trace so only your code is left, fix the accumulator so each order starts clean, and write up the evidence that proves it.
Lab Info
Table of Contents
-
Challenge
Step 1: Meet the Overcharging Nightly Batch Job
The ticket
GloboTicket's admin dashboard runs a nightly batch job that replays every order sitting in the checkout queue through a ledger calculator,
calculateOrderTotal, to work out what each customer owes. A support ticket landed this morning: whenever a batch contains more than one order, every order after the first comes back overcharged, and the overcharge grows the longer the batch runs. The first order in any batch is always correct, which is the strongest clue you have before you open a debugger. info> Feeling stuck? Check out the matchingsolution/stepN/folder for the step you're on to see a working implementation. Give it a try on your own first. The solution folder is your safety net, not your starting point. ### Where the money is calculatedsrc/checkoutLedger.tsexposescalculateOrderTotal(order). For each order it walks the line items, builds up a subtotal, applies the order's discount rate, and rounds the result to cents. That subtotal is a running accumulator, a value that grows as it walks the items. Accumulators are the right tool for this job, but only if each order gets a fresh one. If the accumulator is declared in the wrong place, one order's total can start from where the last order's total left off, and every order after the first inherits a debt it never asked for. ### Why 'just re-run it' isn't enoughA bug that only shows up on the second order of a multi-order batch is easy to lose: run one order at a time in a REPL and it looks perfectly fine. You need a harness that replays the same sequence of orders, in the same order, every time, so the failure is not a one-off you half-remember but a fact you can point at. That harness lives in
src/batchRunner.ts, and building it is the first thing you'll do. ### The planThis lab follows the failure through five moves:
- Reproduce: build a batch runner that replays a fixed order queue so the overcharge happens identically on every run.
- Inspect: capture a breakpoint-style snapshot of the accumulator's locals and call stack at the moment it goes wrong.
- Locate: map the compiled stack frame where things go wrong back to the exact line of TypeScript that produced it.
- Isolate: strip framework-owned frames out of the raw stack trace so only your own code is left to blame.
- Fix and prove: give each order its own accumulator, then assemble a report that ties the culprit frame, the captured evidence, and the corrected totals together.
Each step builds the tool the next one needs. By the time you reach the fix, you won't be guessing at a cause; you'll already have it in hand. ### Running the application
The lab includes a small dashboard,
src/App.tsx, that calls the functions you write. In the Terminal, confirm you're in the folder that containspackage.json, then start it:npm run devThe dev server listens on port 5173. Switch to the Web Browser tab to see the Run Nightly Batch and Diagnose & Verify buttons. Until you finish the tasks, parts of the page show empty states, which is expected.
warning> The lab has a single terminal, so the running server keeps it busy. Press
Ctrl+Cto stop the server before you run any of the commands in the next section, then start it again when you want to look at the page. ### Checking your progress as you workEach task is graded automatically when you complete it, but you can run the same checks yourself at any time. In the Terminal, run any of these:
-
One task, with the task name in quotes:
bash runTest.sh "task 2.1" -
The whole suite:
npx --no-install vitest run --reporter=dot -
The TypeScript type check on its own:
npm run typecheck
Expect failures at first. The starter code is unfinished on purpose, so each task's check fails until you complete that task. If you run the whole suite before you start, you see many failures. That is the normal starting point and does not mean your setup is wrong.
As you complete each task, its check changes from failing to passing. When all ten tasks are done, the whole suite passes.
When a task passes, the summary shows
Tests 1 passed. When it fails, the message afterAssertionErrornames the task and tells you what to change. A clean type check prints no errors after the twonpmlines. The task checks don't look at types, so run the type check yourself before you finish a task. -
Challenge
Step 2: Reproduce the Checkout Ledger Failure on Demand
Making the failure repeatable
Before you can debug anything, you need the failure to happen the same way twice.
src/batchRunner.tsalready exportsSAMPLE_ORDERS, a fixed queue of orders that mirrors what a real nightly batch would see. Your job is to replay that exact queue throughcalculateOrderTotalin the same order every time, so the overcharge is no longer something you half-remember from one run, but something you can reproduce on demand.Return the array instead of logging it, so you and the checks can inspect exactly which totals a run produced. A total's position in the array always identifies the order that produced it.
To check your work, use the commands in "Checking your progress as you work" in Step 1, and press
Ctrl+Cfirst if the dev server is running. ### Seeing where the numbers divergeA single array of totals tells you that something is wrong, but not where it starts going wrong. Tagging each total with the ID of the order that produced it turns the batch's output into a trace you can read top to bottom, watching the moment the first order's clean total gives way to the second order's inflated one.
Keep this pass separate from
runNightlyBatchso the trace reads on its own. ### Watching the overcharge happenThe ledger in
src/checkoutLedger.tsis already running with the bug in it, so the batch you just built can show you the failure. Start the application as described in Step 1, open the Web Browser tab, and click Run Nightly Batch. The queue holds three orders, and their correct totals are $22.50, $19.00 and $32.00. You will seeorder-1come out right, thenorder-2at $42.75 andorder-3at $77.00.Reload the page and click once more: you get the same three numbers. Click twice without reloading and the totals climb even higher, because nothing ever resets the accumulator. That is the failure you will trace, inspect and fix in the next steps.
-
Challenge
Step 3: Capture Breakpoint Evidence from the Ledger
A breakpoint you can replay
A real debugger lets you pause at a line and inspect the locals and the call stack right there. You can get the same value out of a plain function: given a label, a bag of locals, and a call stack, freeze all three into one object you can inspect, store, or compare later. That object is a
BreakpointSnapshot, andcaptureBreakpointinsrc/breakpoint.tsis how you build one.Copying
localsandcallStackmeans a later change to the caller's variables can't alter a snapshot you've already captured.To check your work, use the commands in "Checking your progress as you work" in Step 1, and press
Ctrl+Cfirst if the dev server is running. ### Wiring the snapshot into the ledger's own evidencecheckoutLedger.ts'sapplyDiscountstep already records aDiscountStepRecord(the order ID, the subtotal it was handed, the discount rate and a raw call stack) into a log every time it runs, viagetDiscountStepLog(). That log is real evidence pulled straight from a live run, not a guess.captureLedgerBreakpoints, still insrc/breakpoint.ts, turns every record in that log into a fullBreakpointSnapshotusingcaptureBreakpoint, giving you one inspectable snapshot per order in the batch.Reading the real log, rather than guessing, means each snapshot carries the values that were live at that point, in the order the batch ran.
-
Challenge
Step 4: Read the Stack Trace and Name the Culprit
From compiled line to source line
When a batch runs, any stack trace it produces points at compiled JavaScript, not the TypeScript you actually wrote. A source map is the bridge back: a table that records, for a given compiled file/line/column, which original file, line, column and symbol produced it.
src/sourceMap.tsalready has that table built for you asCOMPILED_TO_SOURCE_MAP; you just need the lookup that uses it.Returning
undefinedfor a miss keeps an unmapped frame distinguishable from a real match.To check your work, use the commands in "Checking your progress as you work" in Step 1, and press
Ctrl+Cfirst if the dev server is running. ### Cutting the noise out of a stack traceA raw call stack captured during a test run is full of frames that belong to the test runner and language internals, not to your code. None of those frames caused the bug, and reading past them to find the one that did wastes the evidence your snapshots hold.
src/stackFilter.tsalready lists the marker strings that identify framework-owned frames inFRAMEWORK_MARKERS; you'll use them to clear those frames out.Return a new array so the original raw trace stays available when you need the full record. ### Naming the one frame responsible
Once the framework frames are gone, what's left should point at your own code, specifically at the function where the accumulator's contaminated value is actually read:
applyDiscount. Turning a filtered trace into a single named culprit frame is what closes the loop between 'the failure happens' and 'here is the line responsible for it.'Filtering first means framework noise can never be picked as the culprit, and the fallback returns a best answer instead of nothing.
-
Challenge
Step 5: Correct the Accumulator and Prove the Fix
Your evidence now points at one cause, and this step closes the loop: you fix the bug, then prove the fix with the same evidence you used to find it. You remove the shared accumulator, then prove the fix in two ways: with a report that ties the culprit frame, the snapshots and the corrected totals together, and with a recomputed batch that shows nothing leaks between orders. By the end, the dashboard reports the corrected totals as verified.
The fix itself
Everything so far has pointed at the same thing:
calculateOrderTotal's subtotal is declared somewhere that survives between calls, so order two starts adding on top of order one's total instead of from zero. The fix is narrow: move the accumulator's declaration inside the function so it's created fresh on every call. It's still the fix, not a workaround, because it removes the shared state entirely rather than resetting it defensively.To check your work, use the commands in "Checking your progress as you work" in Step 1, and press
Ctrl+Cfirst if the dev server is running. ### Writing down the proofA fix that isn't documented is a fix someone else has to rediscover the hard way.
src/diagnosisReport.tsis where the culprit frame from step 4, the breakpoint evidence from step 3, and the corrected totals from the fix all come together into one object, a record that says not just 'it's fixed' but 'here is the exact evidence that it's fixed.'Setting
verifiedonly when all three inputs are complete means a report can't claim to be verified on thin evidence. ### Proving it holds under a second runThe strongest evidence that the shared-accumulator bug is gone isn't a single correct-looking total; it's running the batch twice and getting the identical totals both times. If any state were still leaking between orders, a second run would disagree with the first.
verifyIndependentTotalsmakes that comparison explicit, giving the diagnosis report a check it can point to rather than an assertion to take on faith.Comparing within a small tolerance, rather than exact equality, avoids false failures from floating-point rounding.
-
Challenge
Step 6: Run the Application
Every task is done, and each one was graded as you completed it. What you haven't done yet is watch the whole application run at once, which is the only place the pieces you wrote separately become a single working program.
Run the final checks
Stop the dev server with
Ctrl+Cif it is still running, then run these commands in the Terminal to confirm that the types and the whole suite pass (see Step 1 for the other commands):npm run typecheck npx --no-install vitest run --reporter=dotThe suite ends with
Tests 17 passed (17), and the type check prints no errors.If you want to see what a failure looks like, break something on purpose, watch the matching check fail and then put it back. ### Start the application
In the Terminal, start the dev server again:
npm run devSwitch to the Web Browser tab and click Run Nightly Batch. The totals now read $22.50, $19.00 and $32.00, and they stay the same no matter how many times you click. Click Diagnose & Verify to see the report your code builds: a culprit frame that names
applyDiscount, the breakpoint evidence andVerified: yes.The server reloads on save. When you are finished, press
Ctrl+Cin the terminal to stop it. ### CongratulationsYou turned a vague overcharging ticket into a verified fix. You replayed a fixed queue of orders to make the failure repeatable, mapped a compiled frame back to its TypeScript source, captured breakpoint-style snapshots of the accumulator, filtered out framework frames to name the culprit, and moved the accumulator inside the function so every order started clean.
The same habit works on any shared-state bug: make the failure repeatable, gather evidence where it goes wrong and prove the fix with the same evidence.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.