{"id":"l-at-4-2","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Return expected failures as typed values with a Result union, so callers must handle a declined card or an expired coupon, and keep exceptions for bugs and the truly unexpected.","html":"<p>Here is the signature of the function at the heart of Cartwheel’s checkout:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> placeOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">cart</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Cart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">payment</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentMethod</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> { </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* … */</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span></code></pre></div>\n<p>It can fail in at least five ways the business expects every day: the card is declined, an item is out of stock, the coupon expired, the address is undeliverable, the payment provider asks for 3-D Secure. The signature mentions none of them. They are exceptions, thrown from somewhere inside, and TypeScript has no <code dir=\"ltr\">throws</code> clause. A caller who forgets a <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code> compiles fine, and the customer sees a blank screen.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Making failure part of the type</h2>\n<p>The fix is to return expected failures instead of throwing them, using a discriminated union you already know how to write:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">E</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 style=\"color:#953800;--shiki-dark:#FFA657\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</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 style=\"color:#953800;--shiki-dark:#FFA657\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">error</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</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\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</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 style=\"color:#953800;--shiki-dark:#FFA657\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ ok: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, value });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> err</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">E</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">error</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</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 style=\"color:#953800;--shiki-dark:#FFA657\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">error</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ ok: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, error });</span></span></code></pre></div>\n<p><code dir=\"ltr\">ok</code> is the discriminant. A caller cannot read <code dir=\"ltr\">value</code> without first proving the call succeeded, because <code dir=\"ltr\">value</code> only exists on one member:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> placeOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cart, payment);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">showConfirmation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(result.value);</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Error: Property 'value' does not exist on type 'Result&#x3C;Order, CheckoutError>'.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">//   Property 'value' does not exist on type '{ ok: false; error: CheckoutError; }'.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (result.ok) </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">showConfirmation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(result.value); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// fine</span></span></code></pre></div>\n<h2 id=\"sec-2\" tabindex=\"-1\">Errors with structure</h2>\n<p><code dir=\"ltr\">E</code> could be a string, but a string is the error equivalent of <code dir=\"ltr\">status: string</code>. Model failures as a discriminated union, each with the data needed to handle it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'card-declined'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">declineCode</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'out-of-stock'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">sku</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">available</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'coupon-expired'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">code</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">expiredOn</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> errorMessage</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  switch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (e.type) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'card-declined'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `Your card was declined (${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">declineCode</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}).`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'out-of-stock'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `Only ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">available</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} of ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">sku</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} left.`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    case</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'coupon-expired'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">code</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} expired on ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">e</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expiredOn</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}.`</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<p>The <code dir=\"ltr\">: string</code> return type makes this exhaustive, as you saw in section 1. When the payments team adds <code dir=\"ltr\">'requires-3ds'</code> to the union, every message function and every UI branch that switches on <code dir=\"ltr\">type</code> is flagged.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Composing steps</h2>\n<p>Checkout is a sequence: validate the cart, apply the coupon, charge the card. Each step can fail with its own errors. Early returns compose them without nesting:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CartError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'out-of-stock'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">sku</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">available</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CouponError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'coupon-expired'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">code</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">expiredOn</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> checkout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutInput</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">CartError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CouponError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentError</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\"> cart</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateCart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input.lines);</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\">cart.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cart;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> priced</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> applyCoupon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cart.value, input.coupon);</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\">priced.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> priced;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> charge</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> chargeCard</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(priced.value.total, input.payment);</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\">charge.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> charge;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> ok</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">createOrder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(priced.value, charge.value));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>After <code dir=\"ltr\">if (!cart.ok)</code>, <code dir=\"ltr\">cart</code> is narrowed to <code dir=\"ltr\">{ ok: false; error: CartError }</code>, which is assignable to the function’s wider failure type, so it can be returned as is. The success path reads top to bottom, and the return type documents every failure the caller must handle. If someone later adds a fourth step with a new error type and forgets to widen the return type, the <code dir=\"ltr\">return</code> of that step’s failure is a compile error, so the list of failures cannot silently fall out of date.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 210\" role=\"img\" aria-labelledby=\"l-at-4-2-t1\"><defs><marker id=\"arrow-l-at-4-2-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-at-4-2-t1\">Railway diagram. Three steps, validateCart, applyCoupon and chargeCard, sit on a success track leading to ok Order. From each step a branch drops to a failure track that ends in a Result with ok false and the union of CartError, CouponError and PaymentError.</title>\n  <rect class=\"d-box-accent\" x=\"10\" y=\"40\" width=\"140\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"80\" y=\"69\" text-anchor=\"middle\">validateCart</text>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"40\" width=\"140\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"270\" y=\"69\" text-anchor=\"middle\">applyCoupon</text>\n  <rect class=\"d-box-accent\" x=\"390\" y=\"40\" width=\"140\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"460\" y=\"69\" text-anchor=\"middle\">chargeCard</text>\n  <rect class=\"d-box-success\" x=\"580\" y=\"40\" width=\"110\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"635\" y=\"69\" text-anchor=\"middle\">ok(order)</text>\n  <path class=\"d-arrow\" d=\"M150 64 L196 64\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M340 64 L386 64\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M530 64 L576 64\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M80 88 L80 146\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M270 88 L270 146\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M460 88 L460 146\" marker-end=\"url(#arrow-l-at-4-2-1)\"/>\n  <rect class=\"d-box-warn\" x=\"10\" y=\"150\" width=\"680\" height=\"48\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"350\" y=\"179\" text-anchor=\"middle\">{ ok: false, error: CartError | CouponError | PaymentError }</text>\n</svg><figcaption>Each step either continues on the success track or exits with its error</figcaption></figure>\n<p>You will see libraries that offer <code dir=\"ltr\">map</code> and <code dir=\"ltr\">andThen</code> methods to chain results instead. They are fine, but in TypeScript the early-return style is usually easier to read and needs no library: narrowing does the work.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Wrapping code that throws</h2>\n<p>Most of the failures in a checkout start life as exceptions in someone else’s code: the payment SDK throws a <code dir=\"ltr\">CardError</code>, <code dir=\"ltr\">fetch</code> rejects when the network drops. Convert them once, at the edge, into the <code dir=\"ltr\">Result</code> your own code speaks:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'card-declined'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">declineCode</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</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\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> chargeCard</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">amount</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">payment</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> PaymentMethod</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Receipt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">PaymentError</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\">    return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> ok</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> paymentSdk.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">charge</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(amount, payment));</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\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> unknown</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\"> (e </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">instanceof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CardError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> err</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ type: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'card-declined'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, declineCode: e.code });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    throw</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> e; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// not a failure we understand: let it propagate</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>Three things are worth copying. The <code dir=\"ltr\">catch</code> variable is <code dir=\"ltr\">unknown</code>, as it always is under <code dir=\"ltr\">strict</code>, so you narrow it with <code dir=\"ltr\">instanceof</code> before reading <code dir=\"ltr\">code</code>. Only the one failure the business expects becomes a value; everything else is rethrown untouched, stack trace and all. And the function’s return type is now an honest list of what can go wrong, which every caller sees in a tooltip.</p>\n<p>Asynchronous results nest the obvious way, <code dir=\"ltr\">Promise&lt;Result&lt;T, E&gt;&gt;</code>. A rejected promise still means “something unexpected happened”, and a resolved <code dir=\"ltr\">{ ok: false }</code> means “the business said no”. Keeping those two channels separate is the whole point.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Where exceptions still belong</h2>\n<p><code dir=\"ltr\">Result</code> is for failures the business expects and the caller can act on. It is not a replacement for every <code dir=\"ltr\">throw</code>:</p>\n<ul>\n<li><strong>Bugs and broken invariants</strong> (a <code dir=\"ltr\">null</code> where the types say there cannot be one, an unhandled union member reaching <code dir=\"ltr\">assertNever</code>) should throw. There is nothing sensible for the caller to do, and you want a stack trace.</li>\n<li><strong>Infrastructure failures</strong> you cannot handle locally (the database is down) can propagate to one error boundary or retry layer.</li>\n<li><strong>Third-party code that throws</strong> gets wrapped once at the boundary, converting the exceptions you expect into <code dir=\"ltr\">err(…)</code> and rethrowing the rest.</li>\n</ul>\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>Catching everything into a Result</span></p>\n<p><code dir=\"ltr\">try { … } catch (e) { return err({ type: 'unknown', message: String(e) }) }</code> around a whole function turns bugs into “expected” failures that a UI politely displays and nobody investigates. Convert only the specific failures you understand, such as the payment SDK’s decline error, and let the rest throw.</p>\n</aside>\n<p>In the exercise you will build Cartwheel’s coupon step with <code dir=\"ltr\">Result</code>, compose it into a checkout function, and write the exhaustive error messages. Next lesson deals with the data those steps start from: untrusted input.</p>\n","toc":[{"id":"sec-1","text":"Making failure part of the type","level":2},{"id":"sec-2","text":"Errors with structure","level":2},{"id":"sec-3","text":"Composing steps","level":2},{"id":"sec-4","text":"Wrapping code that throws","level":2},{"id":"sec-5","text":"Where exceptions still belong","level":2}],"takeaways":["A function’s signature says nothing about what it throws, so expected failures hidden in exceptions are easy for callers to forget.","<code dir=\"ltr\">type Result&lt;T, E&gt; = { ok: true; value: T } | { ok: false; error: E }</code> is a discriminated union, so callers must check <code dir=\"ltr\">ok</code> before they can read <code dir=\"ltr\">value</code>.","Model errors as a discriminated union too, so each failure carries its own data and the UI can handle every case exhaustively.","Compose results with early returns (<code dir=\"ltr\">if (!r.ok) return r;</code>); the narrowed failure is assignable to the caller’s wider error union.","Use <code dir=\"ltr\">Result</code> for failures the business expects; keep <code dir=\"ltr\">throw</code> for bugs, broken invariants and infrastructure failures you cannot handle locally."],"further":[{"title":"Discriminated unions (Narrowing)","url":"https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions"},{"title":"Control flow and error handling (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling"}],"quiz":[{"q":"<p><code dir=\"ltr\">placeOrder(cart): Promise&lt;Order&gt;</code> can fail with a declined card. What does its signature tell a caller about that?</p>","options":[{"text":"Nothing; TypeScript has no way to declare what a function throws, so the failure is invisible.","why":"Correct. There is no <code dir=\"ltr\">throws</code> clause in TypeScript, which is why expected failures belong in the return type."},{"text":"That it rejects with an <code dir=\"ltr\">Error</code>, because every promise can reject.","why":"Any promise can reject with any value. The signature does not say which failures are expected or what data they carry."},{"text":"That the caller must wrap it in try/catch, enforced by <code dir=\"ltr\">strict</code>.","why":"No compiler option forces a try/catch; forgetting one compiles fine."},{"text":"That it can throw a <code dir=\"ltr\">CardDeclinedError</code>, inferred from the implementation.","why":"Thrown types are never inferred or tracked, even with all strict flags on."}],"answer":0},{"q":"<p>Why does the last line fail to compile?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">E</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 style=\"color:#953800;--shiki-dark:#FFA657\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</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 style=\"color:#953800;--shiki-dark:#FFA657\">ok</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">error</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> E</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> r</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Result</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">CheckoutError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(r.value.id);</span></span></code></pre></div>\n","options":[{"text":"Because <code dir=\"ltr\">value</code> is optional in <code dir=\"ltr\">Result</code>.","why":"<code dir=\"ltr\">value</code> is required on the success member. The issue is that it does not exist on the failure member at all."},{"text":"Because <code dir=\"ltr\">Result</code> must be awaited first.","why":"<code dir=\"ltr\">r</code> is not a promise in this snippet; nothing needs awaiting."},{"text":"Because <code dir=\"ltr\">value</code> only exists on the <code dir=\"ltr\">ok: true</code> member, and <code dir=\"ltr\">r</code> has not been narrowed.","why":"Correct. Checking <code dir=\"ltr\">if (r.ok)</code> narrows <code dir=\"ltr\">r</code> to the success member, where <code dir=\"ltr\">value</code> is available."}],"answer":2},{"q":"<p>Which failure is a good fit for a <code dir=\"ltr\">Result</code> rather than an exception?</p>","options":[{"text":"A <code dir=\"ltr\">null</code> reaching a function whose types say it cannot be <code dir=\"ltr\">null</code>.","why":"That is a broken invariant, a bug. Throw, and fix the code that let it happen."},{"text":"The database connection pool is exhausted.","why":"Infrastructure failure that the checkout cannot handle locally; let it propagate to the error boundary or retry layer."},{"text":"The coupon code the customer typed has expired.","why":"Correct. It is expected, the caller can do something useful (show a message), and it carries data (the code and its expiry)."}],"answer":2},{"q":"<p><code dir=\"ltr\">validateCart</code> returns <code dir=\"ltr\">Result&lt;Cart, CartError&gt;</code> and <code dir=\"ltr\">checkout</code> returns <code dir=\"ltr\">Result&lt;Order, CartError | PaymentError&gt;</code>. Does this compile?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> cart</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateCart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input);</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\">cart.ok) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cart;</span></span></code></pre></div>\n","options":[{"text":"No, because <code dir=\"ltr\">Result&lt;Cart, CartError&gt;</code> is not <code dir=\"ltr\">Result&lt;Order, …&gt;</code>.","why":"After the check, <code dir=\"ltr\">cart</code> is only the failure member, <code dir=\"ltr\">{ ok: false; error: CartError }</code>, which has no <code dir=\"ltr\">Cart</code> in it."},{"text":"No, you must rebuild the failure with <code dir=\"ltr\">err(cart.error)</code>.","why":"Rebuilding works, but it is unnecessary; the narrowed failure already fits."},{"text":"Only with an <code dir=\"ltr\">as</code> cast.","why":"No cast is needed; assignability handles it."},{"text":"Yes, because the narrowed <code dir=\"ltr\">{ ok: false; error: CartError }</code> is assignable to the wider failure member.","why":"Correct. <code dir=\"ltr\">CartError</code> is part of <code dir=\"ltr\">CartError | PaymentError</code>, so the failure can be returned as is."}],"answer":3}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type Coupon = { code: string; percentOff: number; expiresOn: string };\ntype Cart = { lines: { sku: string; quantity: number; unitPriceCents: number }[] };\ntype PricedCart = Cart & { totalCents: number };\n\nconst COUPONS: Coupon[] = [\n  { code: 'WELCOME10', percentOff: 10, expiresOn: '2099-12-31' },\n  { code: 'SPRING15', percentOff: 15, expiresOn: '2025-05-31' },\n];\nconst STOCK: Record<string, number> = { 'TENT-2P': 4, 'MUG-SET': 0 };\n\n// 1. Define Result, ok and err.\ntype Result<T, E> = unknown;\n\ntype CouponError =\n  | { type: 'coupon-unknown'; code: string }\n  | { type: 'coupon-expired'; code: string; expiredOn: string };\ntype StockError = { type: 'out-of-stock'; sku: string; available: number };\ntype CheckoutError = CouponError | StockError;\n\n// 2. Return a Result instead of throwing.\nfunction findCoupon(code: string, today: string): Coupon {\n  const coupon = COUPONS.find((c) => c.code === code);\n  if (!coupon) throw new Error(`Unknown coupon ${code}`);\n  if (coupon.expiresOn < today) throw new Error(`${code} expired on ${coupon.expiresOn}`);\n  return coupon;\n}\n\nfunction checkStock(cart: Cart): Result<Cart, StockError> {\n  for (const line of cart.lines) {\n    const available = STOCK[line.sku] ?? 0;\n    if (available < line.quantity) return { ok: false, error: { type: 'out-of-stock', sku: line.sku, available } };\n  }\n  return { ok: true, value: cart };\n}\n\n// 3. Compose: stock first, then the coupon (if any), then price the cart.\nfunction checkout(cart: Cart, couponCode: string | null, today: string) {\n  const subtotal = cart.lines.reduce((sum, l) => sum + l.quantity * l.unitPriceCents, 0);\n  const coupon = couponCode ? findCoupon(couponCode, today) : null;\n  const totalCents = coupon ? Math.round(subtotal * (1 - coupon.percentOff / 100)) : subtotal;\n  return { ok: true, value: { ...cart, totalCents } };\n}\n\n// 4. One message per error type.\nfunction errorMessage(e: CheckoutError): string {\n  return 'Something went wrong';\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<ReturnType<typeof findCoupon>, Result<Coupon, CouponError>>>;\ntype T2 = Expect<Equal<ReturnType<typeof checkout>, Result<PricedCart, CheckoutError>>>;\n\nfunction typeTests(r: Result<Coupon, CouponError>) {\n  // @ts-expect-error: value is only available after checking ok\n  r.value.code;\n  if (r.ok) r.value.code;\n}\n\nconst cart: Cart = { lines: [{ sku: 'TENT-2P', quantity: 1, unitPriceCents: 18900 }] };\nconst outcome = checkout(cart, 'SPRING15', '2026-10-05');\nconsole.log(outcome.ok ? outcome.value.totalCents : errorMessage(outcome.error));\n"},"solution":{"ts":"type Coupon = { code: string; percentOff: number; expiresOn: string };\ntype Cart = { lines: { sku: string; quantity: number; unitPriceCents: number }[] };\ntype PricedCart = Cart & { totalCents: number };\n\nconst COUPONS: Coupon[] = [\n  { code: 'WELCOME10', percentOff: 10, expiresOn: '2099-12-31' },\n  { code: 'SPRING15', percentOff: 15, expiresOn: '2025-05-31' },\n];\nconst STOCK: Record<string, number> = { 'TENT-2P': 4, 'MUG-SET': 0 };\n\ntype Result<T, E> = { ok: true; value: T } | { ok: false; error: E };\nconst ok = <T>(value: T): { ok: true; value: T } => ({ ok: true, value });\nconst err = <E>(error: E): { ok: false; error: E } => ({ ok: false, error });\n\ntype CouponError =\n  | { type: 'coupon-unknown'; code: string }\n  | { type: 'coupon-expired'; code: string; expiredOn: string };\ntype StockError = { type: 'out-of-stock'; sku: string; available: number };\ntype CheckoutError = CouponError | StockError;\n\nfunction findCoupon(code: string, today: string): Result<Coupon, CouponError> {\n  const coupon = COUPONS.find((c) => c.code === code);\n  if (!coupon) return err({ type: 'coupon-unknown', code });\n  if (coupon.expiresOn < today) return err({ type: 'coupon-expired', code, expiredOn: coupon.expiresOn });\n  return ok(coupon);\n}\n\nfunction checkStock(cart: Cart): Result<Cart, StockError> {\n  for (const line of cart.lines) {\n    const available = STOCK[line.sku] ?? 0;\n    if (available < line.quantity) return { ok: false, error: { type: 'out-of-stock', sku: line.sku, available } };\n  }\n  return { ok: true, value: cart };\n}\n\nfunction checkout(cart: Cart, couponCode: string | null, today: string): Result<PricedCart, CheckoutError> {\n  const stocked = checkStock(cart);\n  if (!stocked.ok) return stocked;\n\n  const subtotal = cart.lines.reduce((sum, l) => sum + l.quantity * l.unitPriceCents, 0);\n  if (couponCode === null) return ok({ ...cart, totalCents: subtotal });\n\n  const coupon = findCoupon(couponCode, today);\n  if (!coupon.ok) return coupon;\n  return ok({ ...cart, totalCents: Math.round(subtotal * (1 - coupon.value.percentOff / 100)) });\n}\n\nfunction errorMessage(e: CheckoutError): string {\n  switch (e.type) {\n    case 'coupon-unknown': return `We don't recognise the code ${e.code}.`;\n    case 'coupon-expired': return `${e.code} expired on ${e.expiredOn}.`;\n    case 'out-of-stock': return `Only ${e.available} of ${e.sku} left in stock.`;\n  }\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<ReturnType<typeof findCoupon>, Result<Coupon, CouponError>>>;\ntype T2 = Expect<Equal<ReturnType<typeof checkout>, Result<PricedCart, CheckoutError>>>;\n\nfunction typeTests(r: Result<Coupon, CouponError>) {\n  // @ts-expect-error: value is only available after checking ok\n  r.value.code;\n  if (r.ok) r.value.code;\n}\n\nconst cart: Cart = { lines: [{ sku: 'TENT-2P', quantity: 1, unitPriceCents: 18900 }] };\nconst outcome = checkout(cart, 'SPRING15', '2026-10-05');\nconsole.log(outcome.ok ? outcome.value.totalCents : errorMessage(outcome.error));\n"},"checks":[{"id":"types","typecheck":true},{"id":"valid-coupon","test":"const r = checkout({ lines: [{ sku: 'TENT-2P', quantity: 1, unitPriceCents: 10000 }] }, 'WELCOME10', '2026-10-05'); return r.ok === true && r.value.totalCents === 9000;"},{"id":"expired","test":"const r = checkout({ lines: [{ sku: 'TENT-2P', quantity: 1, unitPriceCents: 10000 }] }, 'SPRING15', '2026-10-05'); return r.ok === false && r.error.type === 'coupon-expired' && r.error.expiredOn === '2025-05-31';"},{"id":"stock-first","test":"const r = checkout({ lines: [{ sku: 'MUG-SET', quantity: 1, unitPriceCents: 2500 }] }, 'NOPE', '2026-10-05'); return r.ok === false && r.error.type === 'out-of-stock';"},{"id":"messages","test":"return errorMessage({ type: 'coupon-unknown', code: 'NOPE' }).includes('NOPE') && errorMessage({ type: 'out-of-stock', sku: 'MUG-SET', available: 0 }).includes('MUG-SET') && errorMessage({ type: 'coupon-expired', code: 'SPRING15', expiredOn: '2025-05-31' }).includes('2025-05-31');"}],"title":"Return failures, don't throw them","prompt":"<p>Turn Cartwheel’s coupon step into typed, expected failures:</p>\n<ol>\n<li>Define <code dir=\"ltr\">Result&lt;T, E&gt;</code> as a discriminated union on <code dir=\"ltr\">ok</code>, with <code dir=\"ltr\">ok(value)</code> and <code dir=\"ltr\">err(error)</code> helpers.</li>\n<li>Make <code dir=\"ltr\">findCoupon</code> return <code dir=\"ltr\">Result&lt;Coupon, CouponError&gt;</code>: <code dir=\"ltr\">coupon-unknown</code> when the code does not exist, <code dir=\"ltr\">coupon-expired</code> (with <code dir=\"ltr\">expiredOn</code>) when <code dir=\"ltr\">expiresOn</code> is before <code dir=\"ltr\">today</code>.</li>\n<li>Make <code dir=\"ltr\">checkout</code> return <code dir=\"ltr\">Result&lt;PricedCart, CheckoutError&gt;</code>: check stock first, then the coupon (when <code dir=\"ltr\">couponCode</code> is not <code dir=\"ltr\">null</code>), then return the cart with <code dir=\"ltr\">totalCents</code> (subtotal minus the coupon’s percentage, rounded).</li>\n<li>Make <code dir=\"ltr\">errorMessage</code> return a message for each error type that includes its data (the code, the expiry date, or the SKU).</li>\n</ol>\n","hints":["<code dir=\"ltr\">type Result&lt;T, E&gt; = { ok: true; value: T } | { ok: false; error: E };</code> and <code dir=\"ltr\">const ok = &lt;T&gt;(value: T): { ok: true; value: T } =&gt; ({ ok: true, value });</code>.","In <code dir=\"ltr\">checkout</code>, use early returns: <code dir=\"ltr\">const stocked = checkStock(cart); if (!stocked.ok) return stocked;</code>. The narrowed failure fits the wider <code dir=\"ltr\">CheckoutError</code> union.","Annotate <code dir=\"ltr\">checkout</code>’s return type explicitly, and switch on <code dir=\"ltr\">e.type</code> in <code dir=\"ltr\">errorMessage</code> with a <code dir=\"ltr\">: string</code> return type so a missed case is a compile error."],"explanation":"<p>With <code dir=\"ltr\">Result</code> as a discriminated union, <code dir=\"ltr\">value</code> simply does not exist until a caller checks <code dir=\"ltr\">ok</code>, which is what the <code dir=\"ltr\">@ts-expect-error</code> line proves. <code dir=\"ltr\">findCoupon</code> now declares both of its expected failures in its signature instead of hiding them in <code dir=\"ltr\">throw</code>. Early returns keep <code dir=\"ltr\">checkout</code> flat: each narrowed failure is assignable to <code dir=\"ltr\">CheckoutError</code>, so it is returned unchanged. Because errors are a union with a <code dir=\"ltr\">type</code> discriminant, <code dir=\"ltr\">errorMessage</code> can use each failure’s own data, and adding a new error type later flags this function automatically.</p>\n","checkLabels":{"types":"The file type-checks with zero errors, including all type tests","valid-coupon":"A valid coupon reduces the total (10% off 10000 is 9000)","expired":"An expired coupon returns a <code dir=\"ltr\">coupon-expired</code> failure with its expiry date","stock-first":"Stock is checked before the coupon","messages":"<code dir=\"ltr\">errorMessage</code> includes each failure’s data"}},"words":712,"readMinutes":3}