When a test goes red, the first instinct is to start changing code at random. The professional habit is the opposite: first understand the failure, then fix. Vitest gives a whole set of means for this - from a graphical dashboard to a step debugger - and all of them serve one goal: to see what exactly went wrong and why before touching the implementation. Random fixing is slow and often breaks the neighbor; an understood failure is fixed on the first try.
You start by reading the failure itself. Vitest prints Expected and Received - what was expected and what came - and the line where the assert did not match. Often this is already enough: you see the number is off by one, an object has an extra field, an undefined arrived instead of an array. The call stack leads to the place in the code under test. Before calling in heavy tools, it is worth reading carefully what the runner has already said.
For a visual picture there is the Vitest UI - a dashboard enabled by the --ui flag from the @vitest/ui package. It shows the tree of files and tests, statuses, each test's output, a module graph, and lets you filter and rerun. On a large suite this is handier than a solid terminal sheet: you see which files are affected, how modules connect and what exactly the failed test printed, without scrolling logs.
When a test file is large, you narrow the field of view. test.only leaves a single test in the run, describe.only a single group; test.skip switches off temporarily, and test.todo marks the not-yet-written. This cuts the noise and lets you run only the suspicious. It is important not to forget to remove only before committing - otherwise CI will silently run one test instead of all, and the suite will stop guarding anything.
You can narrow without touching the code too. The -t (--testNamePattern) flag leaves tests whose name matches a pattern, and giving a file path limits the run to one file. Together with watch this gives a fast loop: you edit and instantly see the result for one test, without running the whole suite. Such pointed running is the main way to keep the debugging cycle short until the cause is localized.
For real debugging you attach a debugger. Vitest is launched with --inspect-brk and necessarily with --no-file-parallelism: parallel workers and step debugging are incompatible, files must go one at a time. Then you attach an inspector to the process - from the editor or Chrome DevTools - set a breakpoint and step through, looking at real variable values instead of guesses from logs. This costs more than print debugging but is more precise on a tangled bug.
A common principle stitches all this into a method: reproduce before fixing. Until the failure can be triggered reliably there is nothing to fix - it is unclear what exactly you are fixing. So first you achieve a stable red: narrow to one test, pin the input, read Expected/Received, step with the debugger if needed. Only with a reproduced failure does the edit become meaningful, and the green after it a proof that you fixed exactly that.
The typical failure is fixing by intuition without reading Expected/Received: the edit changes the symptom, not the cause, and the bug surfaces nearby. The second is a forgotten only, because of which CI runs one test and skips regressions in the rest. The third is trying to step with the debugger without --no-file-parallelism and being surprised the breakpoints behave oddly. First reproduce and understand, then fix - in that order debugging is both short and reliable.
describe.only('cart', () => { // run only this group
test.only('sums the items', () => { // and only this test
expect(total(cart)).toBe(4_200) // read Expected/Received
})
test.todo('applies a promo code') // not written yet
})vitest --ui # dashboard: tree, statuses, module graph, output
vitest -t "discount" # only tests with this name
vitest src/cart.test.ts # only one file
# debugger: files one at a time, otherwise breakpoints lie
vitest --inspect-brk --no-file-parallelism src/cart.test.ts