{"id":"l-tj-2-3","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Test code that throws, returns promises or depends on a flaky API, using awaited resolves and rejects assertions and small hand-written stubs.","html":"<p>Splitwise-lite saves expenses over a phone connection on a train. Requests fail. Some failures are worth one retry (a timeout); others are not (the server says the amount is invalid). The code that decides is a dozen lines, and it is exactly the code nobody tests, because “it’s async” and “it needs the API”. Neither is a real obstacle.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The function under test</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/save-expense.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> saveExpense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">expense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expense.description?.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Description is required'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isInteger</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expense.amountCents) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expense.amountCents </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    throw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Amount must be positive'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> api.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">post</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/expenses'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, expense);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (error) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">error.retryable) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> error;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> api.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">post</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/expenses'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, expense); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// one retry</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Because it’s an <code dir=\"ltr\">async</code> function, every <code dir=\"ltr\">throw</code> inside it becomes a <strong>rejected promise</strong>. Callers (and tests) never see a synchronous exception; they see a promise that rejects.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Asserting on promises</h2>\n<p>Vitest waits for a test that is <code dir=\"ltr\">async</code> or returns a promise. Inside, you have two good options:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'resolves with the new id'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> api</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">post</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">saveExpense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Dinner'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, api))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .resolves.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'rejects a blank description'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> api</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">post</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">saveExpense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'  '</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, api))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .rejects.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toThrow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Description is required'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>You can also <code dir=\"ltr\">await</code> the call and assert on the value: <code dir=\"ltr\">const id = await saveExpense(...); expect(id).toBe('e1');</code>. For rejections, prefer <code dir=\"ltr\">rejects</code> over <code dir=\"ltr\">try/catch</code>: if the promise unexpectedly resolves, <code dir=\"ltr\">rejects</code> fails the test, while a <code dir=\"ltr\">try/catch</code> silently skips the <code dir=\"ltr\">catch</code> block and passes. (If you must use <code dir=\"ltr\">try/catch</code>, put <code dir=\"ltr\">expect.assertions(1)</code> at the top so Vitest fails when no assertion ran.)</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Ending the test before the promise settles</span></p>\n<p><code dir=\"ltr\">saveExpense(expense, api).then((id) =&gt; expect(id).toBe('e1'))</code> with no <code dir=\"ltr\">await</code> or <code dir=\"ltr\">return</code> lets the test function finish first. The test is reported green before the assertion runs; at best a confusing unhandled error shows up later in the run, detached from the test that caused it. Vitest 4 protects you in one case: a <code dir=\"ltr\">resolves</code> or <code dir=\"ltr\">rejects</code> assertion you forget to <code dir=\"ltr\">await</code> fails the test. It can’t protect <code dir=\"ltr\">.then</code> callbacks or a <code dir=\"ltr\">try/catch</code> whose <code dir=\"ltr\">catch</code> never runs. The habit that covers all of them: <strong>every promise in a test is awaited.</strong></p>\n</aside>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 220\" role=\"img\" aria-labelledby=\"l-tj-2-3-t1\"><defs><marker id=\"arrow-l-tj-2-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-tj-2-3-t1\">Two timelines. Top: the test returns, then the promise rejects later and nobody is listening. Bottom: the test awaits, so the rejection fails the test.</title>\n  <text class=\"d-label-strong\" x=\"20\" y=\"40\">No await</text>\n  <path class=\"d-line\" d=\"M140 60 L660 60\"/>\n  <circle class=\"d-dot\" cx=\"170\" cy=\"60\" r=\"7\"/>\n  <text class=\"d-label\" x=\"170\" y=\"90\" text-anchor=\"middle\">test starts</text>\n  <rect class=\"d-box-success\" x=\"270\" y=\"44\" width=\"140\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"340\" y=\"65\" text-anchor=\"middle\">test passes</text>\n  <rect class=\"d-box-warn\" x=\"490\" y=\"44\" width=\"160\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"570\" y=\"65\" text-anchor=\"middle\">promise rejects</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"150\">await</text>\n  <path class=\"d-line\" d=\"M140 170 L660 170\"/>\n  <circle class=\"d-dot\" cx=\"170\" cy=\"170\" r=\"7\"/>\n  <text class=\"d-label\" x=\"170\" y=\"200\" text-anchor=\"middle\">test starts</text>\n  <path class=\"d-arrow d-dashed\" d=\"M190 170 L480 170\" marker-end=\"url(#arrow-l-tj-2-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"335\" y=\"160\" text-anchor=\"middle\">waits</text>\n  <rect class=\"d-box-warn\" x=\"490\" y=\"154\" width=\"160\" height=\"32\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"570\" y=\"175\" text-anchor=\"middle\">test fails</text>\n</svg><figcaption>Without await, the test ends before the assertion runs</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">Stubs: collaborators you control</h2>\n<p><code dir=\"ltr\">saveExpense</code> talks to an <code dir=\"ltr\">api</code>. In a test you don’t want a real server; you want an object whose <code dir=\"ltr\">post</code> does whatever this test needs. That object is a <strong>stub</strong>, and you don’t need a library to write one:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Fails once with a retryable error, then succeeds; counts its calls.</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> flakyApi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> calls </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    get</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> calls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> calls; },</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    async</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> post</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      calls </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (calls </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">throw</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">assign</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Error</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Timed out'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), { retryable: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e2'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'retries once after a retryable error'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> api</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> flakyApi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">saveExpense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Taxi'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1800</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, api)).resolves.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'e2'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(api.calls).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>Passing <code dir=\"ltr\">api</code> in as a parameter is what makes this easy. Code that imports a global client deep inside is much harder to test; you meet the tools for that (and their cost) in the next lesson.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Test every branch of the error handling</h2>\n<p>Error handling has more branches than the happy path, and each one is a decision someone made. For <code dir=\"ltr\">saveExpense</code>:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Situation</th>\n<th>Expected</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>API succeeds</td>\n<td>resolves with the id, one call</td>\n</tr>\n<tr>\n<td>Retryable error, then success</td>\n<td>resolves with the id, two calls</td>\n</tr>\n<tr>\n<td>Non-retryable error</td>\n<td>rejects with <strong>that</strong> error, one call</td>\n</tr>\n<tr>\n<td>Invalid input</td>\n<td>rejects with a validation message, zero calls</td>\n</tr>\n</tbody>\n</table></div><p>The third row is the one people skip, and it hides two common bugs: a <code dir=\"ltr\">catch</code> that swallows the error and returns <code dir=\"ltr\">null</code> (callers think the save worked), and a <code dir=\"ltr\">catch</code> that retries everything (a rejected payment charged twice). The rejection alone can’t distinguish the second bug, because the retry fails too. <strong>Counting the calls</strong> can.</p>\n<p>Notice the last row too: invalid input should make <strong>zero</strong> calls. That is a promise about side effects, and it matters. A version that validates <em>after</em> posting would still reject with the right message, so only the call count proves the bad expense never reached the server.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Unhandled rejections fail the run</span></p>\n<p>If code under test starts a promise that rejects and nobody handles it, Vitest reports it as an unhandled error and marks the run as failed, even when every test passed. Treat that message as a real bug report: somewhere a promise is missing an <code dir=\"ltr\">await</code> or a <code dir=\"ltr\">catch</code>.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Timeouts</h2>\n<p>If a promise never settles (a stub that forgets to resolve, a missing <code dir=\"ltr\">await</code> in a loop), the test hangs until Vitest’s timeout, five seconds by default, and fails with a timeout error. Pass a third argument to <code dir=\"ltr\">test</code> for a slower case, or set <code dir=\"ltr\">testTimeout</code> in the config, but treat a slow unit test as a smell: something real is probably leaking in.</p>\n<p>In the exercise you test all four rows of that table and catch four realistic bugs, including the double-charge one. The next lesson replaces the hand-written stubs with <code dir=\"ltr\">vi.fn</code>.</p>\n","toc":[{"id":"sec-1","text":"The function under test","level":2},{"id":"sec-2","text":"Asserting on promises","level":2},{"id":"sec-3","text":"Stubs: collaborators you control","level":2},{"id":"sec-4","text":"Test every branch of the error handling","level":2},{"id":"sec-5","text":"Timeouts","level":2}],"takeaways":["Always <code dir=\"ltr\">await</code> the promise or the assertion on it; a test that ends before the promise settles can pass no matter what the code does.","<code dir=\"ltr\">await expect(promise).rejects.toThrow('message')</code> is the clearest way to test an async failure.","A stub is a tiny fake collaborator you control, such as an api object whose post method succeeds, fails or counts its calls.","Test every branch of error handling, including the one that should give up, and count calls when the difference is how many attempts were made."],"further":[{"title":"Vitest — resolves and rejects","url":"https://vitest.dev/api/expect#rejects"},{"title":"MDN — Using promises","url":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises"},{"title":"Vitest — testTimeout","url":"https://vitest.dev/config/testtimeout"}],"quiz":[{"q":"<p>This test passes, yet the validation in <code dir=\"ltr\">saveExpense</code> was deleted and the call now resolves. Why didn’t the test catch it?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'rejects a blank description'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> saveExpense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ description: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' '</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amountCents: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }, api);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (error) {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(error.message).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Description is required'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">toBe</code> can’t compare error messages; it needs <code dir=\"ltr\">toEqual</code>.","why":"Messages are strings, so <code dir=\"ltr\">toBe</code> compares them correctly; the matcher is never reached here."},{"text":"When the promise resolves, the <code dir=\"ltr\">catch</code> block never runs, so no assertion executes and the test passes.","why":"Correct. Use <code dir=\"ltr\">await expect(...).rejects.toThrow(...)</code>, which fails when the promise resolves, or add <code dir=\"ltr\">expect.assertions(1)</code>."},{"text":"Vitest ignores errors thrown inside <code dir=\"ltr\">catch</code> blocks.","why":"An assertion failing inside <code dir=\"ltr\">catch</code> would fail the test; the problem is that the <code dir=\"ltr\">catch</code> never runs at all."}],"answer":1},{"q":"<p><code dir=\"ltr\">saveExpense</code> should retry once on a retryable error, but give up immediately on any other error. Which test catches a version that retries every error?</p>","options":[{"text":"Stub <code dir=\"ltr\">post</code> to always reject with a non-retryable error and assert the promise rejects.","why":"The buggy version also ends up rejecting (its retry fails too), so this assertion alone can’t tell the versions apart."},{"text":"Stub <code dir=\"ltr\">post</code> to resolve and assert the id is returned.","why":"On the happy path no error happens, so the retry logic never runs."},{"text":"Stub <code dir=\"ltr\">post</code> to reject once with a non-retryable error, count the calls, and assert there was exactly one.","why":"Correct. Both versions reject, but only the buggy one calls the API twice; the call count is the observable difference."}],"answer":2},{"q":"<p>A test that uses the stub <code dir=\"ltr\">{ post: () =&gt; new Promise(() =&gt; {}) }</code> fails after five seconds with a timeout. What happened?</p>","options":[{"text":"The stub returns a promise that never settles, so <code dir=\"ltr\">await saveExpense(...)</code> waits until Vitest’s test timeout.","why":"Correct. A promise with no resolve or reject call stays pending forever; make the stub resolve or reject explicitly."},{"text":"Vitest needs <code dir=\"ltr\">vi.fn()</code> instead of a plain object for any async collaborator.","why":"Plain objects with async methods are perfectly valid stubs; the issue is this one never finishes."},{"text":"The default test timeout is too short for network code and should be raised to 30 seconds.","why":"There is no network here. Raising the timeout only makes the same failure take longer."}],"answer":0}],"exercise":{"kind":"web","mode":"test","subject":"// Saves an expense through the API and resolves with the new id.\n// - Rejects with \"Description is required\" if the description is blank.\n// - Rejects with \"Amount must be positive\" unless amountCents is a\n//   positive integer. Invalid input never reaches the API.\n// - If api.post fails with an error that has { retryable: true },\n//   it tries exactly ONE more time.\n// - Any other API error is passed on: the promise rejects with it.\nasync function saveExpense(expense, api) {\n  if (!expense.description || !expense.description.trim()) {\n    throw new Error('Description is required');\n  }\n  if (!Number.isInteger(expense.amountCents) || expense.amountCents <= 0) {\n    throw new Error('Amount must be positive');\n  }\n  try {\n    const { id } = await api.post('/expenses', expense);\n    return id;\n  } catch (error) {\n    if (!error.retryable) throw error;\n    const { id } = await api.post('/expenses', expense);\n    return id;\n  }\n}\n","mutants":[{"id":"missing-await","code":"// Saves an expense through the API and resolves with the new id.\n// - Rejects with \"Description is required\" if the description is blank.\n// - Rejects with \"Amount must be positive\" unless amountCents is a\n//   positive integer. Invalid input never reaches the API.\n// - If api.post fails with an error that has { retryable: true },\n//   it tries exactly ONE more time.\n// - Any other API error is passed on: the promise rejects with it.\nasync function saveExpense(expense, api) {\n  if (!expense.description || !expense.description.trim()) {\n    throw new Error('Description is required');\n  }\n  if (!Number.isInteger(expense.amountCents) || expense.amountCents <= 0) {\n    throw new Error('Amount must be positive');\n  }\n  try {\n    const { id } = api.post('/expenses', expense);\n    return id;\n  } catch (error) {\n    if (!error.retryable) throw error;\n    const { id } = await api.post('/expenses', expense);\n    return id;\n  }\n}\n"},{"id":"swallows-error","code":"// Saves an expense through the API and resolves with the new id.\n// - Rejects with \"Description is required\" if the description is blank.\n// - Rejects with \"Amount must be positive\" unless amountCents is a\n//   positive integer. Invalid input never reaches the API.\n// - If api.post fails with an error that has { retryable: true },\n//   it tries exactly ONE more time.\n// - Any other API error is passed on: the promise rejects with it.\nasync function saveExpense(expense, api) {\n  if (!expense.description || !expense.description.trim()) {\n    throw new Error('Description is required');\n  }\n  if (!Number.isInteger(expense.amountCents) || expense.amountCents <= 0) {\n    throw new Error('Amount must be positive');\n  }\n  try {\n    const { id } = await api.post('/expenses', expense);\n    return id;\n  } catch (error) {\n    if (!error.retryable) return null;\n    const { id } = await api.post('/expenses', expense);\n    return id;\n  }\n}\n"},{"id":"no-retry","code":"// Saves an expense through the API and resolves with the new id.\n// - Rejects with \"Description is required\" if the description is blank.\n// - Rejects with \"Amount must be positive\" unless amountCents is a\n//   positive integer. Invalid input never reaches the API.\n// - If api.post fails with an error that has { retryable: true },\n//   it tries exactly ONE more time.\n// - Any other API error is passed on: the promise rejects with it.\nasync function saveExpense(expense, api) {\n  if (!expense.description || !expense.description.trim()) {\n    throw new Error('Description is required');\n  }\n  if (!Number.isInteger(expense.amountCents) || expense.amountCents <= 0) {\n    throw new Error('Amount must be positive');\n  }\n  try {\n    const { id } = await api.post('/expenses', expense);\n    return id;\n  } catch (error) {\n    throw error;\n    const { id } = await api.post('/expenses', expense);\n    return id;\n  }\n}\n"},{"id":"retries-everything","code":"// Saves an expense through the API and resolves with the new id.\n// - Rejects with \"Description is required\" if the description is blank.\n// - Rejects with \"Amount must be positive\" unless amountCents is a\n//   positive integer. Invalid input never reaches the API.\n// - If api.post fails with an error that has { retryable: true },\n//   it tries exactly ONE more time.\n// - Any other API error is passed on: the promise rejects with it.\nasync function saveExpense(expense, api) {\n  if (!expense.description || !expense.description.trim()) {\n    throw new Error('Description is required');\n  }\n  if (!Number.isInteger(expense.amountCents) || expense.amountCents <= 0) {\n    throw new Error('Amount must be positive');\n  }\n  try {\n    const { id } = await api.post('/expenses', expense);\n    return id;\n  } catch (error) {\n    \n    const { id } = await api.post('/expenses', expense);\n    return id;\n  }\n}\n"}],"starter":{"js":"const dinner = { description: 'Dinner', amountCents: 4500 };\n\n// A stub API: each call to post runs the next step in the list.\n// A step is either a value to resolve with or an Error to reject with.\nfunction stubApi(...steps) {\n  const api = {\n    calls: 0,\n    async post() {\n      const step = steps[api.calls];\n      api.calls += 1;\n      if (step instanceof Error) throw step;\n      return step;\n    },\n  };\n  return api;\n}\n\nconst retryable = (message) => Object.assign(new Error(message), { retryable: true });\n\ndescribe('saveExpense', () => {\n  test('rejects a blank description without calling the API', async () => {\n    const api = stubApi({ id: 'e1' });\n\n    await expect(saveExpense({ description: ' ', amountCents: 100 }, api)).rejects.toThrow('Description is required');\n    expect(api.calls).toBe(0);\n  });\n\n  // Add: the happy path, a retryable error followed by success,\n  // and a non-retryable error.\n});\n"},"solution":{"js":"const dinner = { description: 'Dinner', amountCents: 4500 };\n\nfunction stubApi(...steps) {\n  const api = {\n    calls: 0,\n    async post() {\n      const step = steps[api.calls];\n      api.calls += 1;\n      if (step instanceof Error) throw step;\n      return step;\n    },\n  };\n  return api;\n}\n\nconst retryable = (message) => Object.assign(new Error(message), { retryable: true });\n\ndescribe('saveExpense', () => {\n  test('rejects a blank description without calling the API', async () => {\n    const api = stubApi({ id: 'e1' });\n\n    await expect(saveExpense({ description: ' ', amountCents: 100 }, api)).rejects.toThrow('Description is required');\n    expect(api.calls).toBe(0);\n  });\n\n  test('resolves with the id from the API', async () => {\n    const api = stubApi({ id: 'e1' });\n\n    await expect(saveExpense(dinner, api)).resolves.toBe('e1');\n    expect(api.calls).toBe(1);\n  });\n\n  test('retries once after a retryable error', async () => {\n    const api = stubApi(retryable('Timed out'), { id: 'e2' });\n\n    await expect(saveExpense(dinner, api)).resolves.toBe('e2');\n    expect(api.calls).toBe(2);\n  });\n\n  test('passes on a non-retryable error without retrying', async () => {\n    const api = stubApi(new Error('Amount too large'), { id: 'e3' });\n\n    await expect(saveExpense(dinner, api)).rejects.toThrow('Amount too large');\n    expect(api.calls).toBe(1);\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-missing-await","suite":"missing-await"},{"id":"catches-swallows-error","suite":"swallows-error"},{"id":"catches-no-retry","suite":"no-retry"},{"id":"catches-retries-everything","suite":"retries-everything"}],"title":"Test every branch of saveExpense","prompt":"<p><code dir=\"ltr\">saveExpense(expense, api)</code> validates the expense, calls <code dir=\"ltr\">api.post</code>, retries <strong>once</strong> on errors marked <code dir=\"ltr\">retryable: true</code>, and passes every other error on. Its rules are in the subject’s comment.</p>\n<p>The starter gives you a <code dir=\"ltr\">stubApi(...steps)</code> helper: each call to <code dir=\"ltr\">post</code> uses the next step, resolving with an object or rejecting with an <code dir=\"ltr\">Error</code>, and <code dir=\"ltr\">api.calls</code> counts the calls. There is also a <code dir=\"ltr\">retryable(message)</code> helper.</p>\n<p>Add tests until your suite catches four hidden bugs: a missing <code dir=\"ltr\">await</code>, a <code dir=\"ltr\">catch</code> that returns <code dir=\"ltr\">null</code> instead of rejecting, a version that never retries, and a version that retries <strong>every</strong> error. Remember to <code dir=\"ltr\">await</code> every <code dir=\"ltr\">resolves</code> and <code dir=\"ltr\">rejects</code>.</p>\n","hints":["Start with the happy path: <code dir=\"ltr\">stubApi({ id: 'e1' })</code> and <code dir=\"ltr\">await expect(saveExpense(dinner, api)).resolves.toBe('e1')</code>. That alone catches the missing await.","For the retry: <code dir=\"ltr\">stubApi(retryable('Timed out'), { id: 'e2' })</code> must resolve with <code dir=\"ltr\">'e2'</code> after two calls.","For a non-retryable error: <code dir=\"ltr\">stubApi(new Error('Amount too large'), { id: 'e3' })</code> must reject with that message <strong>and</strong> <code dir=\"ltr\">api.calls</code> must be 1. The call count states the “don’t retry” rule directly, whatever the stub’s second step does."],"explanation":"<p>The four tests mirror the four branches. The happy path catches the missing <code dir=\"ltr\">await</code>: destructuring a pending promise gives <code dir=\"ltr\">id === undefined</code>. The retry test catches the version that never retries. The non-retryable test does double duty: <code dir=\"ltr\">rejects.toThrow</code> catches the <code dir=\"ltr\">catch</code> that swallows errors (it resolves with <code dir=\"ltr\">null</code>), and <code dir=\"ltr\">expect(api.calls).toBe(1)</code> catches the version that retries everything. If the stub failed on every call, that last bug would be invisible to the rejection assertion, because its retry would fail too; in a real app it is the one that charges a card twice. Here the stub’s <em>second</em> step is a success, so the buggy version also resolves with <code dir=\"ltr\">'e3'</code> and fails the <code dir=\"ltr\">rejects</code> assertion; the call count is what pins the rule no matter how the stub is set up.</p>\n","checkLabels":{"passes":"All your tests pass against the real saveExpense","catches-missing-await":"Catches the version with a missing await","catches-swallows-error":"Catches the version that swallows API errors","catches-no-retry":"Catches the version that never retries","catches-retries-everything":"Catches the version that retries non-retryable errors"}},"words":675,"readMinutes":3}