<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Maksym Kuzmitskyi (MaximusFT)</title>
    <description>The latest articles on DEV Community by Maksym Kuzmitskyi (MaximusFT) (@maximusft).</description>
    <link>https://dev.to/maximusft</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1819104%2Fd158bf22-3ff5-498a-bab8-91ce4b684bc1.jpg</url>
      <title>DEV Community: Maksym Kuzmitskyi (MaximusFT)</title>
      <link>https://dev.to/maximusft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maximusft"/>
    <language>en</language>
    <item>
      <title>What FSD Actually Fixed (and What It Didn't)</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Wed, 09 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/what-fsd-actually-fixed-and-what-it-didnt-3nfg</link>
      <guid>https://dev.to/maximusft/what-fsd-actually-fixed-and-what-it-didnt-3nfg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gu6hg20hbj31eamfg5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gu6hg20hbj31eamfg5o.png" alt="What FSD Actually Fixed (and What It Didn't)" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a long time, "frontend architecture" was almost a contradiction in terms. Backend had layers, domains, bounded contexts, decades of literature. Frontend had &lt;code&gt;components/&lt;/code&gt;, &lt;code&gt;utils/&lt;/code&gt;, and a folder called &lt;code&gt;helpers&lt;/code&gt; that everyone was quietly afraid to open. The general vibe, even among good engineers, was that the frontend didn't need real architecture — it was just where you rendered the thing the backend actually built.&lt;/p&gt;

&lt;p&gt;Feature-Sliced Design showed up into that vacuum, and I think that context matters more than the methodology's specific rules. FSD didn't just propose a folder convention. It made an argument: the frontend is complex enough, and grows unpredictably enough, that it deserves the same seriousness backend systems get. That argument is the actual contribution, and it's the reason FSD spread as fast as it did — not the specific layer names.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FSD's real achievement wasn't a folder structure. It was convincing an entire ecosystem that the frontend was worth architecting at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've &lt;a href="https://ma-x.im/blog/smeared-component" rel="noopener noreferrer"&gt;written before&lt;/a&gt; about how following FSD's rules literally, without thinking, produced one of the worse codebases I've built — a single button smeared across ten folders, each one "correctly" following the layer it was assigned to. That article was about the failure mode. This one is about something narrower: which problem FSD was actually solving, and where the credit for "clean architecture" quietly gets misattributed to a folder convention that never promised it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What FSD actually fixed
&lt;/h2&gt;

&lt;p&gt;Three things, and they're real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It gave teams a shared vocabulary for growth.&lt;/strong&gt; Before FSD, "where does this go" was answered differently by every senior engineer on the team, usually based on whatever they'd seen at their last job. FSD gives you &lt;code&gt;pages&lt;/code&gt;, &lt;code&gt;features&lt;/code&gt;, &lt;code&gt;entities&lt;/code&gt;, &lt;code&gt;shared&lt;/code&gt; — not perfect names, but names everyone on a team can agree mean the same thing. That alone kills a huge amount of bikeshedding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It made import direction a rule instead of a convention.&lt;/strong&gt; The layer hierarchy — shared can't import from entities, entities can't import from features, and so on — is enforceable with a lint rule. That's the part I actually think is underrated. Most frontend codebases don't have architectural violations because someone made a bad call; they have them because nothing stopped the bad call from compiling. FSD turned "please don't import upward" from a code review comment into a build failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It gave "the frontend has architecture" a name people could put on a job posting.&lt;/strong&gt; This sounds cynical, but I mean it as a genuine point: before FSD (and a handful of contemporaries), it was hard to even have the conversation about frontend architecture in a hiring or planning context, because there wasn't a shared reference point. Now there is. That's a real, if unglamorous, win.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it never promised to fix
&lt;/h2&gt;

&lt;p&gt;Here's where I think people — including a version of me, a few years back — get it wrong. FSD tells you &lt;em&gt;where&lt;/em&gt; a piece of code should live relative to other pieces. It does not tell you &lt;em&gt;whether the pieces you're splitting apart should have been split apart in the first place.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters because the smeared-component failure isn't a bug in FSD. It's what happens when you use a layering convention to answer a question it was never designed to answer: how much of a single feature's logic should live together versus be distributed. FSD's layers describe &lt;em&gt;horizontal&lt;/em&gt; concerns — what kind of thing is this (a hook, a type, an API call) — and following them faithfully will happily scatter every piece of a single &lt;em&gt;vertical&lt;/em&gt; concern (one feature, one component, one story) across the whole hierarchy, because each piece genuinely does belong to a different layer by FSD's own rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// "Correct" FSD placement for one payment-method component
src/
  features/payment-method/model.ts // the hook and state
  entities/payment-method/types.ts // the type
  shared/hooks/useDebounce.ts // a helper it needs
  shared/api/paymentMethods.ts // the fetch call
  features/analytics/paymentTracking.ts // its analytics event

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those placements passes an FSD lint rule. None of them help the next engineer who needs to change how the payment method selector behaves, because understanding it now requires reconstructing it from five files across three layers, in a codebase that will tell you, correctly, that nothing is architecturally wrong.&lt;/p&gt;

&lt;p&gt;FSD also doesn't fix cohesion at the &lt;em&gt;page&lt;/em&gt; level, and it doesn't fix team discipline. A team that argues about everything else will still argue about whether a given piece of logic is a &lt;code&gt;feature&lt;/code&gt; or an &lt;code&gt;entity&lt;/code&gt; — FSD gives you a vocabulary for the argument, not an answer to it. And a team with no code review discipline will produce a messy FSD codebase exactly as fast as it would have produced a messy flat one; the folders will just be tidier while it happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The credit-misattribution problem
&lt;/h2&gt;

&lt;p&gt;I think this is the actual thing worth naming: teams that adopt FSD often experience a real improvement in their codebase, and then attribute all of it to the layering. Some of the improvement is the layering. A meaningful chunk of it is something else entirely — the simple fact that adopting &lt;em&gt;any&lt;/em&gt; deliberate structure, discussed and agreed on as a team, forces the conversations that were previously being skipped. "Where does this go" gets asked out loud instead of guessed at. That conversation is valuable regardless of which methodology prompted it.&lt;/p&gt;

&lt;p&gt;Which means the honest version of "we adopted FSD and our codebase got better" is often "we adopted a shared convention, discussed it as a team, and enforced it with tooling — and FSD happened to be the convention we picked." That's still a win. It's just a different, more general win than "FSD solved our architecture problem," and conflating the two is how teams end up surprised when FSD alone doesn't prevent a smeared component, a bloated &lt;code&gt;shared/&lt;/code&gt; folder, or a &lt;code&gt;pages/&lt;/code&gt; layer that's turned into a dumping ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually reach for now
&lt;/h2&gt;

&lt;p&gt;I still use FSD's structure as a starting point — &lt;a href="https://ma-x.im/blog/react-playbook-code-structure" rel="noopener noreferrer"&gt;the version I use in practice&lt;/a&gt; keeps the layer names but treats them as a coarse sort, not a mandate to split every feature into its smallest constituent parts. The rule I actually enforce is closer to: a feature's pieces get distributed across layers only when another feature genuinely needs to share them. Until then, they stay together, even if that means a &lt;code&gt;features/payment-method/&lt;/code&gt; folder that internally holds its hook, its types, and its API call in one place, technically "violating" the purist reading of the layer boundaries.&lt;/p&gt;

&lt;p&gt;That's not a rejection of FSD. It's treating it as what it actually is: a good answer to "how do we agree on where things go as this project grows," and a tool that says nothing useful about "how much should stay together." Knowing which question you're asking is the part that doesn't come in the docs.&lt;/p&gt;

&lt;p&gt;If your team adopted FSD and it didn't fix the thing you hoped it would, I'd genuinely ask which of the two problems you actually had — because they need different fixes, and only one of them is solved by a folder structure.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>fsd</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Checkout Isn't a Form. It's the Only Part of the App Where Architecture Has a Stopwatch on It</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sun, 06 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/checkout-isnt-a-form-its-the-only-part-of-the-app-where-architecture-has-a-stopwatch-on-it-18l0</link>
      <guid>https://dev.to/maximusft/checkout-isnt-a-form-its-the-only-part-of-the-app-where-architecture-has-a-stopwatch-on-it-18l0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f1rdzqpy6t7r8p45a7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f1rdzqpy6t7r8p45a7n.png" alt="Checkout Isn't a Form. It's the Only Part of the App Where Architecture Has a Stopwatch on It" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've spent a real chunk of my career around purchase flows — one-click purchase, multi-step checkout, the whole family of screens between "I want this" and "I bought this." And the thing that took me the longest to internalize wasn't a pattern or a library. It was what we're actually selling in that flow.&lt;/p&gt;

&lt;p&gt;It isn't the UI. It isn't even the product, at that point — the user already decided on the product two screens ago. What we're selling is &lt;em&gt;time-to-goal&lt;/em&gt;. How fast, with how little friction, does this person get from "I'm ready to buy" to "it's done"? Every metric that actually matters downstream — conversion, cart abandonment, repeat purchases — is a proxy for that one number.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The product in a checkout flow isn't the form. It's the distance between intent and confirmation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds like a UX statement, and it is one. But I want to argue it's also, maybe primarily, an &lt;em&gt;architecture&lt;/em&gt; statement — because the UX-y stuff people file under "polish" (responsiveness, one-click purchase, graceful recovery from a dropped connection) is downstream of decisions made in how the checkout is built, not decisions made in Figma.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every step is round-trip debt
&lt;/h2&gt;

&lt;p&gt;Here's the mental model I use. Every screen, every confirmation, every "are you sure" in a checkout flow is a withdrawal against the user's patience. Doesn't matter how pretty it is. A step is a step.&lt;/p&gt;

&lt;p&gt;Multi-step checkouts get justified all the time — shipping, then payment, then review — and sometimes that's genuinely the right shape. But I'd push back on treating that as free. Each transition is a network round trip if it's fetching anything, a re-render if it isn't, and either way it's a moment where the user can get pulled away, get a spinner they don't trust, or hit the back button and lose state they already entered.&lt;/p&gt;

&lt;p&gt;The architectural question isn't "how do we make step 2 nice." It's "does step 2 need to exist as a separate step at all, or is it three fields we could've asked for on step 1 without anyone noticing the difference." That's a product conversation, sure, but it's the frontend architect's job to keep asking it, because engineers are the ones who feel the cost of &lt;em&gt;not&lt;/em&gt; asking it — in state management, in the number of places cart data can go stale, in every edge case around "what if they refresh here."&lt;/p&gt;

&lt;h2&gt;
  
  
  One-click purchase is a state-management problem wearing a UX costume
&lt;/h2&gt;

&lt;p&gt;One-click purchase looks, from a design brief, like "remove the button presses." Architecturally, it's a much harder problem: you're compressing an entire multi-step flow's worth of validation, payment, and inventory checks into a single optimistic action, and you have to do it without ever showing the user a screen that says "wait."&lt;/p&gt;

&lt;p&gt;That means the hard part isn't the click. It's everything that has to already be true &lt;em&gt;before&lt;/em&gt; the click for the click to be safe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OneClickEligibility&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;hasValidPaymentMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hasCompleteShippingAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;itemInStock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;priceUnchangedSinceLastSync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canOneClickPurchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OneClickEligibility&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasValidPaymentMethod&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasCompleteShippingAddress&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemInStock&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priceUnchangedSinceLastSync&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything hinges on that check being &lt;em&gt;current&lt;/em&gt; at the moment of the click, not current as of when the page loaded. If price or stock data is stale by even a few seconds, "one click" turns into "one click, then a screen apologizing that the price changed" — which is worse than a normal checkout, because you promised speed and delivered a rug-pull.&lt;/p&gt;

&lt;p&gt;So the real architectural commitment behind one-click purchase isn't a button component. It's a decision to keep a small slice of critical state (price, stock, payment validity) continuously fresh in the background, so the click can be trusted the instant it happens. That's a background-sync and cache-invalidation problem, not a UI problem, and it needs to be treated as first-class — not bolted on after the button ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimistic UI, but with a real rollback story
&lt;/h2&gt;

&lt;p&gt;Checkout is one of the few places where optimistic UI actually earns its complexity, because the alternative — making someone stare at a spinner while you confirm a card charge — is its own kind of failure. But optimistic UI without a serious rollback path is just a faster way to lie to the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useSubmitOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderDraft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// show success-leaning state immediately&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// the user already saw a hopeful state — the recovery message&lt;/span&gt;
      &lt;span class="c1"&gt;// has to explain what changed, not just that something failed&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;submitOrder&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail that actually matters here is the comment. If you show a confident "confirming your order" state and then it fails, the failure message can't be a generic toast. The user's mental model has already moved forward — they think they're done. Rolling that back cleanly, with a message that says what actually happened (card declined vs. item went out of stock vs. network timeout), is architecture work: it means your error states need to carry &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;that&lt;/em&gt;, all the way from the API layer up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responsiveness is a checkout feature, not a checkout nice-to-have
&lt;/h2&gt;

&lt;p&gt;I'd draw a hard line here: on most of an app, a layout that reflows awkwardly on a weird viewport is a bug you triage next sprint. On checkout, it's a lost sale, because a meaningful chunk of purchases happen one-handed, on a phone, possibly in a moving vehicle, possibly with one bar of signal.&lt;/p&gt;

&lt;p&gt;That changes the priority order of what you architect for. Payment fields need to work with autofill without fighting the browser. Buttons need to be large enough that a shaky hand doesn't mis-tap into "cancel." And the layout needs to survive the keyboard eating half the screen on mobile, because "the confirm button is off-screen when the keyboard is open" is a shockingly common way to lose an order that was otherwise complete.&lt;/p&gt;

&lt;p&gt;None of that is exotic. It's just a different priority order than the rest of the app gets, and if your component architecture treats checkout screens as "just more pages," they'll get the same generic responsive treatment as everything else — which is to say, good enough for a blog post, not good enough for a payment form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric that should be driving these decisions
&lt;/h2&gt;

&lt;p&gt;Here's the part I actually want to land, because it's the thing that reframes all of the above from "best practices" into an actual measurement problem.&lt;/p&gt;

&lt;p&gt;If the product is time-to-goal, then the thing worth instrumenting isn't page views or even conversion rate in isolation — it's the &lt;em&gt;distribution&lt;/em&gt; of time between "entered checkout" and "confirmed order," broken down by step, and the drop-off at each step. That's a genuinely different question than "does this page load fast," and it points architecture in a specific direction: every step needs a timestamp, every abandonment needs a last-known-step, and every retry needs to be attributable to a cause (validation error, payment decline, network failure, user just left).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trackCheckoutStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CheckoutStep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;advanced&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abandoned&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;errored&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout_step&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a small function. The architectural commitment behind it — instrumenting every step consistently, from the same source of truth as the state machine driving the flow, not bolted on separately by whoever remembers to add tracking — is the actual work. Get that right and you have a real answer to "where is friction actually happening," instead of a guess dressed up as a redesign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the architecture conversation
&lt;/h2&gt;

&lt;p&gt;None of this is about picking the right library for multi-step forms. It's about recognizing that checkout is the one part of the app where the business metric (completed purchase) and the architecture metric (time-to-goal, state consistency across steps, recovery from failure) are the same number wearing two names. Everywhere else in the app, you can separate "is this well-architected" from "does this convert." In checkout, you mostly can't — a badly architected checkout &lt;em&gt;is&lt;/em&gt; a checkout with worse conversion, because every extra round trip, every stale price, every optimistic update with no honest rollback is friction the user feels as delay between wanting the thing and having it.&lt;/p&gt;

&lt;p&gt;If you're the one arguing for a cleaner checkout architecture and getting pushback that it's "just implementation detail" — it isn't. I'd make the case that of everything in the app, this is the part where the architecture review and the conversion review should be the same meeting.&lt;/p&gt;

&lt;p&gt;I've got a lot more from this part of my career than fits in one article — one-click purchase edge cases, retry strategies for flaky payment providers, the exact way stale cart state causes support tickets. If there's a piece of this you want me to go deeper on, tell me which one and I'll write it.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>checkout</category>
      <category>architecture</category>
      <category>ux</category>
    </item>
    <item>
      <title>Most Advanced TypeScript Isn't Worth What It Costs</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/most-advanced-typescript-isnt-worth-what-it-costs-32l1</link>
      <guid>https://dev.to/maximusft/most-advanced-typescript-isnt-worth-what-it-costs-32l1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzuwogavu7a54o2td3ent.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzuwogavu7a54o2td3ent.png" alt="Most Advanced TypeScript Isn't Worth What It Costs" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me start with an admission, because it shapes everything below: I'm not especially strong at advanced TypeScript. Conditional types nested three deep, recursive template literal parsers, the kind of thing that shows up in a library's internals and gets applauded on Twitter — I read that code slowly, and I don't enjoy writing it.&lt;/p&gt;

&lt;p&gt;For a while I treated that as a gap to close. Now I mostly treat it as a constraint worth designing around, and I think that's the more useful position. Not because type-level programming isn't impressive. Because on an application codebase, a type that only one person on the team can modify is a liability wearing the costume of rigor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ma-x.im/blog/react-playbook-typescript-patterns" rel="noopener noreferrer"&gt;The earlier TypeScript article&lt;/a&gt; covered the patterns I reach for constantly — typing the API boundary, discriminated unions for async state, inference at the router. This one is about the question that comes &lt;em&gt;after&lt;/em&gt; you know the patterns: how much type complexity should a given piece of code carry, and how do you tell when you've overshot?&lt;/p&gt;

&lt;h2&gt;
  
  
  Types have a running cost
&lt;/h2&gt;

&lt;p&gt;The pitch for TypeScript is that types catch bugs. True, and I'd never go back. But the framing hides something: a type isn't a one-time purchase. It's a thing every future reader has to understand before they can safely change the code underneath it.&lt;/p&gt;

&lt;p&gt;So each type has a cost, and it shows up in three places. Someone has to read it to understand the contract. Someone has to modify it when requirements move. And when it goes wrong, someone has to decode the error message — which, for a sufficiently clever type, is a wall of text that names none of the things you actually wrote.&lt;/p&gt;

&lt;p&gt;That last one deserves more weight than it usually gets. A type that produces an unreadable error at the call site has partly defeated its own purpose. The whole point was to tell someone they made a mistake. If the message needs its own investigation, you haven't caught the bug so much as relocated it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The value of a type is not how much it proves. It's how much it proves &lt;em&gt;per unit of understanding it demands&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That ratio is the thing I actually optimize for now, and it explains why my list of "worth it" is short.&lt;/p&gt;

&lt;h2&gt;
  
  
  What earns its keep
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Discriminated unions, everywhere.&lt;/strong&gt; This is the highest-value construct in application TypeScript and it's barely "advanced" at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PolicyRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines, and an entire class of bug is now unrepresentable — you cannot read &lt;code&gt;policies&lt;/code&gt; without first proving you're in the success branch. The compiler narrows it for you, the errors are legible, and any developer can extend it without a conversation. Cheap to read, expensive bugs prevented. That's the ratio you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branded types, but only where mixups are real.&lt;/strong&gt; If your codebase passes around several kinds of ID as bare strings and you've actually shipped a bug from swapping two of them, this is worth it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PolicyId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;__brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PolicyId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CustomerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;__brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CustomerId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the condition. &lt;em&gt;If you've actually shipped that bug.&lt;/em&gt; Branding every primitive in the app because it's theoretically safer is how you end up with casts scattered everywhere and a team quietly annoyed at you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;satisfies&lt;/code&gt; instead of annotation, when you want both.&lt;/strong&gt; This one is underused and it's almost free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routePermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;write&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Permission&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the constraint checked &lt;em&gt;and&lt;/em&gt; the literal types preserved, so &lt;code&gt;routePermissions.billing&lt;/code&gt; is &lt;code&gt;['read']&lt;/code&gt; rather than a widened &lt;code&gt;Permission[]&lt;/code&gt;. No cleverness required, real inference gained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generics on components, one parameter deep.&lt;/strong&gt; A typed &lt;code&gt;DataTable&amp;lt;TRow&amp;gt;&lt;/code&gt; that infers the row type from the data you pass it is genuinely good. Two or three interdependent type parameters with constraints referencing each other is where it stops paying, in my experience — the signature becomes the thing people copy-paste rather than read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What usually doesn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Type-level logic that reimplements runtime logic.&lt;/strong&gt; Deep conditional types that compute a shape based on five flags. Recursive types that parse a string format into a structure. These are legitimately impressive and they belong in libraries, where a small number of maintainers absorb the complexity so thousands of users don't have to. In application code the arithmetic is inverted: your whole team pays, nobody outside benefits, and the requirements will change next quarter anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Types that exist to avoid a small runtime check.&lt;/strong&gt; Sometimes the honest answer is a validation at the boundary and a plain type afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;policySchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've now got a real guarantee — checked against actual data, not just asserted about it — and the type downstream is boring. I'd take that over an elaborate type that describes what the server &lt;em&gt;should&lt;/em&gt; send, every time. The type system can't see your API. A parser can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generic abstractions built for one call site.&lt;/strong&gt; The rule I'd apply here is the same one that applies to component abstractions: don't generalize until you have the second case in front of you. A generic hook with three type parameters serving exactly one consumer is a puzzle you built for yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test I actually use
&lt;/h2&gt;

&lt;p&gt;When I'm not sure whether a type has gone too far, I ask two things.&lt;/p&gt;

&lt;p&gt;First: &lt;em&gt;can a mid-level developer on this team change the code this type guards, without asking me?&lt;/em&gt; If the honest answer is no, the type has become a bottleneck with my name on it. That's not safety — it's a bus factor problem I introduced on purpose.&lt;/p&gt;

&lt;p&gt;Second: &lt;em&gt;what does the error look like when someone gets it wrong?&lt;/em&gt; I'll take a slightly weaker type with a clear failure message over a stronger one that produces forty lines of inference noise. The error message is the user interface of a type, and it deserves the same consideration as any other interface.&lt;/p&gt;

&lt;p&gt;Neither test involves how much the type proves. That's deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I've landed
&lt;/h2&gt;

&lt;p&gt;I used to think being weak at type-level programming meant I'd eventually hit a ceiling in TypeScript. What actually happened is that the ceiling turned out to be somewhere I don't need to go very often — and that most of the safety I care about comes from a handful of unglamorous constructs applied consistently at the right places, mostly at the edges where data enters the app.&lt;/p&gt;

&lt;p&gt;The interesting decisions aren't in the type system at all. They're about &lt;em&gt;where&lt;/em&gt; you put the boundary, what you validate, and which mistakes you've decided are worth making impossible. Once those are right, the types tend to stay simple on their own. When I find myself writing something genuinely gnarly, it's usually a sign the design underneath it is doing something it shouldn't.&lt;/p&gt;

&lt;p&gt;I'm aware this is the position of someone who isn't a type wizard, and I'd take the counter-argument seriously. So if you've got a case where deep type-level machinery genuinely paid for itself in an application — not a library — I want to see it. That's the example that would move me.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Your Agent Instructions Are Rotting Right Now</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/your-agent-instructions-are-rotting-right-now-3e2</link>
      <guid>https://dev.to/maximusft/your-agent-instructions-are-rotting-right-now-3e2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsiqlpjdoiitvqzgcu5kd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsiqlpjdoiitvqzgcu5kd.png" alt="Your Agent Instructions Are Rotting Right Now" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a particular kind of file that only ever grows. You know the one. It started as a short list of project conventions for your agent, and every time something went sideways you appended a line to stop it happening again. &lt;em&gt;Always run the build before committing. Don't touch the lockfile. Use the existing helper instead of writing a new one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every line was correct on the day it was written. That's what makes this hard to see.&lt;/p&gt;

&lt;p&gt;Because instructions aren't code. Nothing fails when they go stale. There's no red test, no type error, no failing pipeline. A rule that stopped being true six months ago sits there looking exactly like a rule that's still true, and the agent — being obedient — follows both with equal conviction.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instructions don't break loudly. They just quietly stop describing your project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://ma-x.im/blog/agent-playbook-context-is-the-product" rel="noopener noreferrer"&gt;The context is the product&lt;/a&gt; made the case that what you feed an agent matters more than which model you picked. This is the uncomfortable follow-up: that context is an artifact you own, and artifacts you own need maintenance. Nobody budgets for maintaining a text file.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the rot actually happens
&lt;/h2&gt;

&lt;p&gt;It's never one dramatic mistake. It's four small, reasonable things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The project moves and the file doesn't.&lt;/strong&gt; You migrate from one test runner to another, rename a directory, drop a library. The code changes in one commit. The instruction describing the old world changes in... no commit, because nobody thought about it. Now the agent is confidently steering toward a folder that isn't there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules get added to fix single incidents.&lt;/strong&gt; The agent did something dumb once, so you wrote a rule. The rule is narrow, situational, and phrased as a universal law. Twenty of those later, you've encoded a list of past accidents rather than a description of how the project works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing ever gets deleted.&lt;/strong&gt; Deleting a rule feels risky — what if it was load-bearing? So the file only accretes. And because instructions usually sit near the top of the agent's context, every dead line is taking up room that live information could be using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contradictions arrive silently.&lt;/strong&gt; Rule 4 says prefer the shared utility. Rule 19, added months later by someone solving a different problem, says write it locally in the feature folder. Both are in the file. The agent picks one, more or less arbitrarily, and now its behavior looks random — which is the symptom people usually misdiagnose as the model being unreliable.&lt;/p&gt;

&lt;p&gt;That last one is worth sitting with. When an agent behaves inconsistently across similar tasks, the instinct is to blame the model or add &lt;em&gt;another&lt;/em&gt; rule. Often the real cause is that you've given it two rules and asked it to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptoms, before you go looking
&lt;/h2&gt;

&lt;p&gt;You can usually feel this before you can point at it.&lt;/p&gt;

&lt;p&gt;The agent starts doing things you don't remember asking for, and when you grep the instructions, there it is — a line from months ago you'd completely forgotten writing. Or it does the right thing four times out of five, and the fifth is a coin flip. Or you find yourself correcting the same class of thing in review over and over, adding a clarification each time, and the clarifications aren't helping. That's the tell: if adding rules isn't improving behavior, the problem isn't a missing rule. It's the pile.&lt;/p&gt;

&lt;p&gt;Honestly, the strongest signal is simpler. When was the last time you read your instructions file top to bottom? If you can't remember, you're not maintaining a document, you're maintaining a sediment layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat it like code, because it is
&lt;/h2&gt;

&lt;p&gt;The fix isn't clever. It's just deciding that this file has an owner and a lifecycle, the same as any other part of the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it end to end on a schedule.&lt;/strong&gt; Not when something breaks — on a cadence. It takes ten minutes. You will find at least one line that describes a project that no longer exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete aggressively.&lt;/strong&gt; This is the part people won't do, so let me put it plainly: a rule you can't justify today is doing damage today. It's consuming context, and it's a coin-flip waiting to happen when it eventually contradicts something newer. If you're wrong and it mattered, you'll find out fast and can put it back in thirty seconds. That's a cheap mistake. Carrying twenty dead rules is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer describing the system over listing incidents.&lt;/strong&gt;"Feature code lives under &lt;code&gt;src/features/{feature}&lt;/code&gt;, and anything shared moves to &lt;code&gt;src/shared&lt;/code&gt; only when a second feature needs it" is worth ten rules about specific files. Principles survive refactors. Incident notes don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hunt contradictions directly.&lt;/strong&gt; When you review, don't just ask &lt;em&gt;is this line true?&lt;/em&gt; Ask &lt;em&gt;does this line disagree with another line?&lt;/em&gt; Contradictions are the expensive failure here, and they're invisible unless you're specifically looking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update instructions in the same commit as the change.&lt;/strong&gt; You renamed the folder — the rule mentioning that folder is part of the rename. This is the one habit that stops rot at the source, and it costs nothing once it's automatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  I've had to redo this myself
&lt;/h2&gt;

&lt;p&gt;I'm not writing this from the outside. The memory system on this site is exactly this problem in a different shape — a set of files an agent reads before it does anything, describing conventions and decisions.&lt;/p&gt;

&lt;p&gt;I wrote up the first version, published it, and then &lt;a href="https://ma-x.im/blog/ai-agent-memory-redesign" rel="noopener noreferrer"&gt;redesigned the whole thing after a few weeks of actually using it&lt;/a&gt;. Not because the original idea was wrong, but because the first design let information accumulate without ever forcing anything out. The rules that ended up mattering were about &lt;em&gt;overwriting&lt;/em&gt; rather than appending: this file gets rewritten in full, that entry gets edited in place rather than duplicated, completed items get deleted rather than archived. Not because deletion is elegant. Because anything that only grows eventually stops being read — by me, and by the agent.&lt;/p&gt;

&lt;p&gt;That's the lesson I'd extract for instructions generally. The interesting design question isn't what to write down. It's what forces something to leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small is a feature
&lt;/h2&gt;

&lt;p&gt;There's a version of this article that ends with "so audit your rules regularly," and that's true but weak. The stronger claim is that the size of an instruction file is itself a quality signal, in the wrong direction.&lt;/p&gt;

&lt;p&gt;A short, current, internally consistent set of rules beats a comprehensive one, every time. The agent reads all of it. You can hold all of it in your head. When something's wrong you can find it. A long file fails on all three, and it fails invisibly, which is the worst property a piece of configuration can have.&lt;/p&gt;

&lt;p&gt;So the question I'd ask about your instructions isn't whether they're thorough. It's whether you'd be comfortable reading them out loud to a new engineer joining tomorrow — and how many lines you'd catch yourself apologizing for on the way through.&lt;/p&gt;

&lt;p&gt;If you've found a maintenance rhythm that actually sticks — something better than "review it when the agent embarrasses you" — I'd like to hear it. That's the part I'm still refining.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>They Asked Me to Build a Slider. It Was a Much Better Question Than I Thought.</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sun, 30 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/they-asked-me-to-build-a-slider-it-was-a-much-better-question-than-i-thought-1hk7</link>
      <guid>https://dev.to/maximusft/they-asked-me-to-build-a-slider-it-was-a-much-better-question-than-i-thought-1hk7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9gk87rx8fulb66f9kye.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9gk87rx8fulb66f9kye.png" alt="They Asked Me to Build a Slider. It Was a Much Better Question Than I Thought." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I sat down for a technical interview recently and got handed this: here's a React page, here are ten image URLs from the internet, build a slider. Next button, previous button, a row of dots underneath. Click a dot, jump straight to that slide. No animation needed.&lt;/p&gt;

&lt;p&gt;My honest first reaction was &lt;em&gt;is this it?&lt;/em&gt; I'm an architect. I'd come in expecting to defend some structural decision, argue about boundaries, maybe get grilled on a system design. Instead I got a task I'd have called kindergarten-level on paper.&lt;/p&gt;

&lt;p&gt;Then I started typing, and about thirty seconds in I got genuinely interested — not in the slider, in what the slider was doing to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thirty seconds
&lt;/h2&gt;

&lt;p&gt;Before writing anything I asked questions. Do you want keyboard support? Should it wrap around? Do the images need preloading? Some of those got a &lt;em&gt;no, don't worry about it&lt;/em&gt;. Fine — that's an answer, and now the scope is pinned down instead of assumed.&lt;/p&gt;

&lt;p&gt;Then I opened the component and the first thing I wrote was state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCurrentSlide&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One value. That's all this thing needs to know about itself — which slide is showing. Everything else is derived.&lt;/p&gt;

&lt;p&gt;And the second thing I wrote was three empty functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleNext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePrevious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSelectSlide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty. Nothing inside them. I hadn't decided the boundary behavior yet, hadn't written a single &lt;code&gt;div&lt;/code&gt;. I just named the three things this component can &lt;em&gt;do&lt;/em&gt;, and then kept going.&lt;/p&gt;

&lt;p&gt;I want to be careful here, because this is the part I actually find interesting and it would be easy to oversell. I'm not claiming that's the One True Order. But sitting there, watching myself do it, I realized I had described the entire component — its state and its complete behavior surface — before rendering anything. The markup afterwards was almost mechanical. There was nothing left to decide.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The interesting part of a trivial task isn't whether you finish it. It's what you reach for first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same task, three ways
&lt;/h2&gt;

&lt;p&gt;I've never sat on the other side of the table scoring people on this, so take what follows as a hypothesis rather than a verdict. But I think this task separates approaches cleanly, and the separation has nothing to do with knowing React.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start from the markup.&lt;/strong&gt; Drop in a &lt;code&gt;div&lt;/code&gt;, put an &lt;code&gt;img&lt;/code&gt; in it, add two buttons. Then wire up the next button — oh, that needs a handler, scroll back up, write one. That needs state — scroll up again, add &lt;code&gt;useState&lt;/code&gt;. Then the dots, which need to know the active index, which means going back to think about what the index actually means. The code arrives, but it arrives by accretion, each piece pulling the previous one apart a little.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start from the pieces, one at a time.&lt;/strong&gt; State first, then a handler, then the markup for that handler, then the next handler. Better — but each behavior is decided in isolation, so the boundary rules ("what happens at the last slide?") get discovered one at a time, in the middle of writing JSX, when you're least set up to think about them consistently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start from state and behavior as a set.&lt;/strong&gt; One &lt;code&gt;useState&lt;/code&gt;, three named handlers, all declared before anything renders. The boundary question surfaces immediately and gets answered once, for all three, because they're sitting right next to each other. Then you render.&lt;/p&gt;

&lt;p&gt;The difference isn't skill with React. All three people finish. The difference is whether the shape of the component was decided up front or discovered by bumping into it, and &lt;em&gt;that&lt;/em&gt; is the thing this dumb little task exposes with surprising precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then they made it worse, and that's when it got good
&lt;/h2&gt;

&lt;p&gt;Part two: show three images per page instead of one. And add looping — from the last page, next goes to the first; from the first, previous goes to the last.&lt;/p&gt;

&lt;p&gt;That sounds like a small tweak. It isn't, and this is the bit I enjoyed.&lt;/p&gt;

&lt;p&gt;In the one-image version, the natural move at the end is to disable the button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleNext&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Next
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boundary is handled in the &lt;em&gt;markup&lt;/em&gt;, as a disabled state. Now change the rules. With three-per-page and wrapping, "the end" isn't the last image anymore, it's the last &lt;em&gt;page&lt;/em&gt; — and there's no such thing as the end, because it wraps. The button is never disabled. The logic moves out of the JSX and into the handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imagesPerPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleNext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setCurrentPage&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePrevious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setCurrentPage&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things changed underneath, and neither is visible in the requirement as stated. The unit of state stopped being &lt;em&gt;which image&lt;/em&gt; and became &lt;em&gt;which page&lt;/em&gt;, which is a different concept that happens to look the same on screen with ten images and one per page. And the boundary rule migrated from a rendering concern to a behavioral one.&lt;/p&gt;

&lt;p&gt;If your first version had the wrapping logic smeared across the JSX, this is where you pay. If it was already sitting inside three named handlers, you edit two lines.&lt;/p&gt;

&lt;p&gt;Ten images, three per page, and suddenly the task has an actual opinion about your first draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Say it out loud or none of this counts
&lt;/h2&gt;

&lt;p&gt;Here's the thing that makes the whole exercise work, and it's not the code.&lt;/p&gt;

&lt;p&gt;Nobody watching can see you reason. They see a cursor. So I narrated the entire time: &lt;em&gt;I'll start with state — I think all I need to store is which slide is selected. Now the handlers. I'm not going to build arrow-key navigation unless you want it, just next and previous buttons.&lt;/em&gt; And the answer came back: yeah, that's fine, don't bother.&lt;/p&gt;

&lt;p&gt;That exchange took four seconds and it removed a chunk of work I would otherwise have guessed at. More importantly, it made my reasoning visible. Someone watching me could tell &lt;em&gt;why&lt;/em&gt; the code was landing in that order, which is information they cannot get any other way.&lt;/p&gt;

&lt;p&gt;This is the same argument I made in &lt;a href="https://ma-x.im/blog/interviewing-senior-engineers" rel="noopener noreferrer"&gt;the piece on interviewing senior engineers&lt;/a&gt; from the interviewer's side: the signal is in the reasoning, not the artifact. Sitting on the candidate side of it, I'd put it even more bluntly. If you think silently and produce correct code, you've handed over the least interesting half of what you know.&lt;/p&gt;

&lt;h2&gt;
  
  
  And yes, I used Google
&lt;/h2&gt;

&lt;p&gt;At one point I needed to iterate a fixed number of times to render the dots. The simplest thing is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dot&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleSelectSlide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I blanked on it. Couldn't remember how to make an empty array of a given length. So I asked whether I could look it up, they said sure, I looked it up, and moved on.&lt;/p&gt;

&lt;p&gt;I'd like that to be unremarkable, because it should be. Forgetting a piece of syntax you type maybe twice a year says nothing about anything. What would actually have mattered is if I hadn't known &lt;em&gt;what I was reaching for&lt;/em&gt; — that I needed a fixed-length iteration, that the dots derive from page count rather than being their own state. That part I never doubted. The incantation to produce the array is a lookup.&lt;/p&gt;

&lt;p&gt;The failure mode isn't forgetting an API. It's not knowing what shape of thing you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;p&gt;I don't know how the interview went. Genuinely — no result yet, and I'm writing this without one.&lt;/p&gt;

&lt;p&gt;But I came away thinking the task was much smarter than it looked, and I'd been slightly snobbish about it in the first minute. A hard problem mostly tells you whether someone has seen that problem before. A trivial problem, watched closely, tells you the order someone's mind moves in — and then a small twist tells you whether their first draft was structured or just correct.&lt;/p&gt;

&lt;p&gt;The slider was never the point. It was a transparent object they could watch me think through, and it cost them ten minutes to set up.&lt;/p&gt;

&lt;p&gt;If you're interviewing people and reaching for something elaborate, I'd at least consider the opposite: take something almost insultingly simple, watch the first thirty seconds, then change one requirement. And if you've been on the receiving end of a task like this and read it as disrespect — I did too, briefly. I'd like to hear whether you changed your mind the way I did.&lt;/p&gt;

</description>
      <category>interviewing</category>
      <category>career</category>
      <category>react</category>
      <category>engineeringculture</category>
    </item>
    <item>
      <title>React Gives You the Loading State. It Does Not Give You Cancellation.</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/react-gives-you-the-loading-state-it-does-not-give-you-cancellation-53g2</link>
      <guid>https://dev.to/maximusft/react-gives-you-the-loading-state-it-does-not-give-you-cancellation-53g2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs91hbh1jbzggt5m0g8ad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs91hbh1jbzggt5m0g8ad.png" alt="React Gives You the Loading State. It Does Not Give You Cancellation." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ma-x.im/blog/react-playbook-error-handling" rel="noopener noreferrer"&gt;error handling article&lt;/a&gt; argued that failures are a product decision — what breaks, how loudly, and what the user does next. There's one failure mode it didn't cover, and it's the one that produces the weirdest bug reports. Not "the request failed." The opposite: the request &lt;em&gt;succeeded&lt;/em&gt;, just too late, and nobody wanted the answer anymore.&lt;/p&gt;

&lt;p&gt;React 19 made the front half of this much nicer. &lt;code&gt;useActionState&lt;/code&gt; gives you the pending flag without a single &lt;code&gt;useState&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;submitSearch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchPoliciesAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three values, one line, and the loading state is handled. That used to be four lines of ceremony and at least one bug where &lt;code&gt;setLoading(false)&lt;/code&gt; didn't run on the error path. Real improvement.&lt;/p&gt;

&lt;p&gt;But look at what &lt;code&gt;isPending&lt;/code&gt; actually tells you. It says React is still waiting on &lt;em&gt;this&lt;/em&gt; action. It says nothing about the fetch you fired ninety milliseconds ago that's still in flight somewhere over the Atlantic. React flipped a boolean. The network doesn't know that happened.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A spinner that stops is a UI event. A request that stops is a network event. React only does the first one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The bug that survives &lt;code&gt;isPending&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Type into a search box. Every keystroke fires a request. Responses come back whenever they feel like it — the one for "poli" leaves first, the one for "policy" leaves second and returns first, then "poli" lands and overwrites it. Now the input says &lt;em&gt;policy&lt;/em&gt; and the list below shows results for &lt;em&gt;poli&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The UI is not loading. The UI is not erroring. The UI is confidently wrong, and it stays wrong until the user touches something.&lt;/p&gt;

&lt;p&gt;This is a race, and pending state cannot fix a race. &lt;code&gt;isPending&lt;/code&gt; was true, then false, exactly as designed. What you needed was for the earlier request to &lt;em&gt;stop existing&lt;/em&gt; the moment it stopped mattering. That's the job AbortController does, and it's the reason I think it deserves a chapter of its own rather than a footnote in a data-fetching post.&lt;/p&gt;

&lt;p&gt;Honestly, I think the main reason this stays exotic is the name. &lt;code&gt;AbortController&lt;/code&gt; sounds like something you'd find in an operating systems textbook. The actual API is three things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole API, in about ten lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/policies&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You make a controller, you hand its &lt;code&gt;signal&lt;/code&gt; to whatever is doing the async work, and calling &lt;code&gt;abort()&lt;/code&gt; tells that work to give up. The pending &lt;code&gt;fetch&lt;/code&gt; rejects immediately.&lt;/p&gt;

&lt;p&gt;Two details worth knowing up front, because they're where people trip.&lt;/p&gt;

&lt;p&gt;First, a signal is single-use. Once a controller is aborted, it stays aborted forever — you don't reset it, you make a new one. One controller per request, not one per component.&lt;/p&gt;

&lt;p&gt;Second, aborting rejects the promise. It doesn't quietly resolve to nothing. Which brings us to the mistake I'd bet is the single most common one in this whole area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation is not a failure
&lt;/h2&gt;

&lt;p&gt;When you abort a fetch, it throws. If you have a normal &lt;code&gt;try/catch&lt;/code&gt; around it, that &lt;code&gt;catch&lt;/code&gt; runs, and unless you say otherwise the user gets an error state for something &lt;em&gt;you&lt;/em&gt; deliberately caused.&lt;/p&gt;

&lt;p&gt;I've seen this produce red toasts on perfectly healthy apps. Navigate away from a page mid-load, and the app cheerfully informs you that something went wrong. Nothing went wrong. You left.&lt;/p&gt;

&lt;p&gt;So the rule that matters more than any other in this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;DOMException&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// we cancelled this on purpose — not a user-facing failure&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An abort is an instruction you gave. It should never reach your error UI, your toast system, or Sentry. Treat it as control flow, not as a fault.&lt;/p&gt;

&lt;p&gt;There's a newer, cleaner way to express the same check if you already have the signal at hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea, less &lt;code&gt;instanceof&lt;/code&gt; archaeology. Both are fine. What's not fine is letting the abort fall through into the same branch as a 500.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small version that covers most cases
&lt;/h2&gt;

&lt;p&gt;Here's the shape I'd reach for by default. A ref holding the current controller, aborted right before the next request starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PolicySearchPanel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AbortController&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchPolicies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/policies?query=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchPolicies&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"query"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines do the real work. &lt;code&gt;inFlight.current?.abort()&lt;/code&gt; kills the previous request before starting a new one, so the stale response can never land. The &lt;code&gt;useEffect&lt;/code&gt; cleanup aborts on unmount, so navigating away doesn't leave a request writing into a component that no longer exists.&lt;/p&gt;

&lt;p&gt;That's the universal, boring version. It isn't clever and it doesn't need to be. Most cancellation bugs I can think of are covered by exactly those two moments: &lt;em&gt;something newer started&lt;/em&gt;, and &lt;em&gt;the thing that wanted this is gone&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Notice what &lt;code&gt;useActionState&lt;/code&gt; is and isn't doing here. It owns &lt;code&gt;isPending&lt;/code&gt; and the result value, which is genuinely less code than before. The cancellation is still entirely yours. The new hooks didn't make this obsolete — they made the missing half more visible, because now the only thing you're hand-rolling &lt;em&gt;is&lt;/em&gt; the abort.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal has to reach the bottom
&lt;/h2&gt;

&lt;p&gt;Here's where this stops being a component concern and becomes an architecture one.&lt;/p&gt;

&lt;p&gt;Most apps don't call &lt;code&gt;fetch&lt;/code&gt; in components. They call &lt;code&gt;policiesApi.search(query)&lt;/code&gt;, which calls a shared &lt;code&gt;httpClient&lt;/code&gt;, which eventually calls &lt;code&gt;fetch&lt;/code&gt;. If any layer in that chain doesn't take a signal, cancellation dies there — and you'll be sitting in the component wondering why abort does nothing.&lt;/p&gt;

&lt;p&gt;So the API layer needs to pass it through, all the way down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;searchPolicies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/policies?query=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An optional &lt;code&gt;signal&lt;/code&gt; on every async function that touches the network. It costs nothing at the call sites that don't care, and it's the difference between cancellation being available and being theoretically available.&lt;/p&gt;

&lt;p&gt;This is also why I'd rather teach the primitive than the library helper. TanStack Query already hands your query function a signal — you just have to &lt;em&gt;use&lt;/em&gt; it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;policies&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;searchPolicies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That signal is there in every TanStack Query app in the world, and a lot of them pass it nowhere. The library already solved the plumbing. If your own layers drop the signal on the floor, the plumbing has nothing to plug into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two extras that are worth knowing
&lt;/h2&gt;

&lt;p&gt;Timeouts, without the &lt;code&gt;setTimeout&lt;/code&gt; dance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And combining reasons to stop — say, "cancel if the user navigates away &lt;em&gt;or&lt;/em&gt; if eight seconds pass":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timeout abort surfaces as a &lt;code&gt;TimeoutError&lt;/code&gt; rather than an &lt;code&gt;AbortError&lt;/code&gt;, which is exactly what you want: one of those deserves a message to the user, the other doesn't. That distinction is the whole game — &lt;em&gt;why&lt;/em&gt; did this stop, and does the person staring at the screen need to know?&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually remember
&lt;/h2&gt;

&lt;p&gt;Strip it down and there are maybe five things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One controller per request. Never reuse an aborted one.&lt;/li&gt;
&lt;li&gt;Abort the previous request before starting the next one.&lt;/li&gt;
&lt;li&gt;Abort on unmount.&lt;/li&gt;
&lt;li&gt;Never show an abort in the UI. It's control flow, not an error.&lt;/li&gt;
&lt;li&gt;Accept an optional &lt;code&gt;signal&lt;/code&gt; in every async function that reaches the network, or the chain breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing there is advanced. It's five habits, and once they're muscle memory you stop writing an entire category of bug — the stale-write, the ghost update, the error toast on a page you already left.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I keep coming back to
&lt;/h2&gt;

&lt;p&gt;What strikes me about React 19's async story is how much it improved the &lt;em&gt;visible&lt;/em&gt; half of the problem. Pending state used to be where the boilerplate lived, and now it mostly isn't. That's real, and I'd take it every time.&lt;/p&gt;

&lt;p&gt;But it makes the asymmetry sharper. The framework got better at telling you it's waiting, and no better at stopping the thing it's waiting for — because it can't. React owns the component tree. It doesn't own the network. The moment you fire a request, you've created something that outlives React's opinion of it, and the only way to reel that back in is to have kept a handle on it.&lt;/p&gt;

&lt;p&gt;AbortController is that handle. It's fifteen lines of very unglamorous code, and I'd argue it's the difference between an app that &lt;em&gt;looks&lt;/em&gt; responsive and one that's actually consistent with what the user is doing right now.&lt;/p&gt;

&lt;p&gt;If you've got a cancellation pattern that's held up better than the ref-and-cleanup version above — especially in bigger apps where requests fan out across several layers — I'd genuinely like to see it. Send it over.&lt;/p&gt;

</description>
      <category>react</category>
      <category>errors</category>
      <category>abortcontroller</category>
      <category>async</category>
    </item>
    <item>
      <title>Evaluation Is the Missing Loop in Agent Workflows</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/evaluation-is-the-missing-loop-in-agent-workflows-260p</link>
      <guid>https://dev.to/maximusft/evaluation-is-the-missing-loop-in-agent-workflows-260p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rn4ug710nwdbfh8me2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rn4ug710nwdbfh8me2z.png" alt="Evaluation Is the Missing Loop in Agent Workflows" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Agent workflows are unusually easy to improve by accident.&lt;/p&gt;

&lt;p&gt;You change an instruction. The next run looks good. You add another rule. A different task breaks. You give the model a stronger prompt. The original failure disappears, but a new one appears somewhere else.&lt;/p&gt;

&lt;p&gt;After a few weeks, you have a growing pile of instructions and no reliable answer to the only question that matters: is the agent actually better?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you do not replay the same work, you are measuring your mood about the last run, not the quality of the system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The demo is not an evaluation
&lt;/h2&gt;

&lt;p&gt;A successful demo proves that one path worked once. Agent workflows need more than that because the system is probabilistic and stateful. Small changes in context, tool output, or task wording can produce a different path.&lt;/p&gt;

&lt;p&gt;An evaluation starts with representative work. Real tickets. Real repository structure. Real conventions. Not a toy prompt that makes the workflow look clean.&lt;/p&gt;

&lt;p&gt;The task should have a clear expected result, but not necessarily one exact sequence of actions. If the agent takes a different safe path and produces the right change, that should count as success. Evaluate the outcome and the important constraints, not theatrical obedience.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to measure
&lt;/h2&gt;

&lt;p&gt;Correctness comes first: did the implementation solve the task? Then check the engineering constraints: did it touch the right boundary, preserve existing behavior, run the required checks, and produce a reviewable change?&lt;/p&gt;

&lt;p&gt;There are operational measures too. How many tool calls did the run take? How often did it ask for help? How much did it cost? Did it retry the same action? Did it stop at the right boundary?&lt;/p&gt;

&lt;p&gt;You do not need a giant scorecard. Start with five or ten tasks that represent the work you actually care about and a short list of pass conditions for each.&lt;/p&gt;

&lt;p&gt;A task can pass while still teaching you something. The agent may have reached the result through a fragile route, used twice as many calls as necessary, or relied on a lucky document lookup. Record those details. Evaluation is not just a gate; it is a source of better design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep failures concrete
&lt;/h2&gt;

&lt;p&gt;"The agent was confused" is not a useful failure record. Write what happened: it selected the wrong package because two tools had overlapping names; it guessed a story-point estimate when the ticket was ambiguous; it changed a shared component without checking its consumers.&lt;/p&gt;

&lt;p&gt;Then classify the failure. Was context missing? Was a tool too broad? Was the instruction ambiguous? Was the permission wrong? Did the task itself lack an expected outcome?&lt;/p&gt;

&lt;p&gt;This classification keeps the fix proportional. Do not add a paragraph of prompt text to solve a permission problem. Do not add a new tool to solve a missing acceptance criterion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the workflow, not only the final diff
&lt;/h2&gt;

&lt;p&gt;The final code can look correct while the path to it is unsafe. An agent may edit production configuration, ignore a required check, or make a destructive action and then repair the result. The diff hides that story.&lt;/p&gt;

&lt;p&gt;For important workflows, record the trace: the decisions, tool calls, approvals, retries, and stop conditions. You are looking for behavior that will fail on a slightly different task.&lt;/p&gt;

&lt;p&gt;This is where the previous articles meet. The context should be sufficient. The tools should be narrow. The agent should ask at sharp edges. The guardrails should cap the run. Evaluation is the feedback loop that tells you which of those parts is weak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not overfit the benchmark
&lt;/h2&gt;

&lt;p&gt;A benchmark can become another form of theater. If you keep rewriting the test until the agent passes, you are measuring how well the test has adapted to the agent.&lt;/p&gt;

&lt;p&gt;Keep some tasks private or rotate them. Add a fresh real task occasionally. Compare the agent's performance against a human review standard, not only against yesterday's score.&lt;/p&gt;

&lt;p&gt;Also keep a manual path. An agent that passes a benchmark but makes work harder for the person reviewing it is not helping. The system should reduce cognitive load, not move the same uncertainty to the final reviewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;Prompt editing feels like progress because the feedback is immediate. Evaluation feels slower because it asks you to repeat work you already did. That repetition is the point. Without it, every improvement is anecdotal and every regression can hide in a different task.&lt;/p&gt;

&lt;p&gt;I would rather have a small honest suite of real tasks than a dashboard full of synthetic numbers. The goal is not to prove that the agent is intelligent. It is to know whether it can perform the specific work I am asking it to perform, within the boundaries I am willing to give it.&lt;/p&gt;

&lt;p&gt;Take the last five agent tasks that required correction. Turn them into a tiny evaluation set. Write down what success means, replay them after the next change, and keep the failures visible. That is how an agent workflow becomes an engineered system instead of a collection of hopeful instructions.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>Memory Is Not a Bigger Context Window</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/memory-is-not-a-bigger-context-window-21hb</link>
      <guid>https://dev.to/maximusft/memory-is-not-a-bigger-context-window-21hb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjwbtcsxoj7bwjny67wh6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjwbtcsxoj7bwjny67wh6.png" alt="Memory Is Not a Bigger Context Window" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The easiest way to misunderstand agent memory is to treat it as a transcript that never ends.&lt;/p&gt;

&lt;p&gt;Keep every message. Keep every tool call. Keep every correction. Eventually the agent will know everything that happened.&lt;/p&gt;

&lt;p&gt;It will also have to search through everything that happened before making a simple decision, and it will not know which old decision is still valid. That is not memory. It is an attic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Memory is not everything the agent has seen. It is what the next run needs to know without making the same mistake again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A transcript records; memory changes the baseline
&lt;/h2&gt;

&lt;p&gt;A transcript is valuable for audit and debugging. It tells you what the agent did, what tools returned, and where the run went wrong. But a future session does not need the whole conversation. It needs the durable result.&lt;/p&gt;

&lt;p&gt;The branch convention belongs in memory. The abandoned approach probably does not. The decision to ask before estimating an unclear ticket belongs in memory. The five speculative alternatives discussed before that decision probably do not.&lt;/p&gt;

&lt;p&gt;This distinction matters because a transcript is chronological while a working memory should be organized around use. The agent needs current facts, active constraints, decisions, and lessons. It does not need to relive every path that led there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four useful kinds of memory
&lt;/h2&gt;

&lt;p&gt;I would keep project memory small and explicit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Facts&lt;/strong&gt; describe stable reality: where the application lives, which command runs the checks, which service owns an endpoint, which branch is the production branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decisions&lt;/strong&gt; record choices that future work must respect: why one workspace contains several repositories, why a boundary is intentionally strict, why a tool is read-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons&lt;/strong&gt; capture failures and their prevention: update main before branching, do not use a broad mutation tool, stop when the same check fails repeatedly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tasks&lt;/strong&gt; describe what is active now. They should expire. A completed task is not a fact about the project and should not remain forever just because it once mattered.&lt;/p&gt;

&lt;p&gt;The categories are useful because they give old information an expiration policy. Facts change slowly. Tasks change quickly. Lessons should be revisited when the workflow changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current beats historical
&lt;/h2&gt;

&lt;p&gt;The hardest memory bug is stale truth. An agent reads an old instruction that sounds authoritative and follows it after the codebase has moved on.&lt;/p&gt;

&lt;p&gt;Every durable memory item needs an owner and a way to become obsolete. Keep dates where they help. Prefer one current entry over a pile of corrections. Remove a rule when the system no longer needs it. A memory store that only grows becomes another source of context pollution.&lt;/p&gt;

&lt;p&gt;This is the same principle as code. Dead code is not harmless because it is still readable. Dead instructions are not harmless because an agent can still follow them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write memory at the correction point
&lt;/h2&gt;

&lt;p&gt;The most useful time to update memory is immediately after a mistake has been understood. Not after a month of similar failures. Not as a quarterly cleanup exercise.&lt;/p&gt;

&lt;p&gt;The entry should be short and operational: what happened, why it happened, and what rule prevents it. "The build failed" is not a lesson. "The sitemap imports every post, so an unescaped backtick in any content field breaks the build" is a lesson because it changes the next action.&lt;/p&gt;

&lt;p&gt;This is how the agent improves without pretending the model itself has permanently changed. The next session inherits the correction because the environment carries it forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory needs boundaries too
&lt;/h2&gt;

&lt;p&gt;Not every project should share the same memory. A global preference can be useful. A repository-specific deployment rule can be dangerous when applied elsewhere. Scope matters: user memory, workspace memory, project memory, and task memory should not all be one undifferentiated folder.&lt;/p&gt;

&lt;p&gt;The agent should know where a rule came from and how far it applies. A local convention is not a universal law. A temporary workaround is not architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;A bigger context window lets an agent carry more text. Good memory lets it carry the right lessons.&lt;/p&gt;

&lt;p&gt;Those are different capabilities. One delays forgetting. The other improves the starting point.&lt;/p&gt;

&lt;p&gt;When I add something to agent memory now, I ask whether it will change a future decision. If not, it probably belongs in a log, a document, or nowhere. Memory is a control surface for behavior, not a museum of conversations.&lt;/p&gt;

&lt;p&gt;Open your current agent instructions and find one paragraph that no longer changes what the agent does. Delete it. Then find the last correction you repeated twice and turn it into one short rule. That is a better memory system than a thousand more lines of transcript.&lt;/p&gt;

&lt;p&gt;The final question is how to know whether these rules actually improve the agent. Good intentions are not an evaluation strategy.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>When an Agent Should Ask Instead of Acting</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sat, 22 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/when-an-agent-should-ask-instead-of-acting-55kf</link>
      <guid>https://dev.to/maximusft/when-an-agent-should-ask-instead-of-acting-55kf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe3cejyid2vi5x9z0fzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe3cejyid2vi5x9z0fzk.png" alt="When an Agent Should Ask Instead of Acting" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most frustrating agent is not the one that asks too many questions. It is the one that asks about harmless details and confidently guesses at the dangerous ones.&lt;/p&gt;

&lt;p&gt;"Which branch should I use?" after you have defined the convention is noise. "Should I push this change to production?" is exactly the question you want.&lt;/p&gt;

&lt;p&gt;That difference is the whole design problem. An agent should not be maximally autonomous. It should be autonomous in the places where its decisions are clear, reversible, and within the rules you have agreed on.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Autonomy is not a volume knob. It is a map of decisions, boundaries, and consequences.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Not every uncertainty matters equally
&lt;/h2&gt;

&lt;p&gt;An agent can be uncertain about a variable name and still make a safe choice. It should not be equally free to guess about an unknown business rule, a missing permission, or a destructive operation.&lt;/p&gt;

&lt;p&gt;I find it useful to separate decisions into three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the intent clear?&lt;/li&gt;
&lt;li&gt;Is the action reversible?&lt;/li&gt;
&lt;li&gt;Is the consequence contained?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When all three answers are comfortable, the agent can act. When one is uncertain, it may need a check. When several are uncertain, it should stop and ask before touching anything important.&lt;/p&gt;

&lt;p&gt;This gives autonomy a shape. The agent is not trusted because it sounds confident. It is trusted because the decision sits inside a boundary we understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the safe path obvious
&lt;/h2&gt;

&lt;p&gt;Asking is not automatically good. An agent that responds to every small ambiguity with a question simply moves the work back to you. The goal is not to eliminate judgment. It is to reserve human judgment for the decisions where it has the highest value.&lt;/p&gt;

&lt;p&gt;That means documenting defaults. If the branch name follows a convention, the agent should know it. If a missing estimate means "ask only when the ticket is genuinely unclear," write that down. If a draft pull request is safe but merging requires approval, make the two states distinct.&lt;/p&gt;

&lt;p&gt;The agent should not ask because it lacks courage. It should ask because the workflow tells it this decision belongs to a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use staged actions
&lt;/h2&gt;

&lt;p&gt;A dangerous action becomes easier to govern when it is split into stages. Instead of one tool that changes production, expose inspection, preview, and apply as separate steps.&lt;/p&gt;

&lt;p&gt;The agent can read the current state. It can prepare the proposed change. It can show the effect. A person can approve the final operation.&lt;/p&gt;

&lt;p&gt;This pattern is useful far beyond deployment. A pull request can be drafted before it is merged. A database migration can be generated and checked before it runs. A bulk update can produce a count and sample before it changes records.&lt;/p&gt;

&lt;p&gt;The agent keeps momentum through the reversible work. The human appears at the sharp edge, where a mistake becomes expensive to undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask with a decision, not a shrug
&lt;/h2&gt;

&lt;p&gt;There is a big difference between "I am not sure what to do" and a useful escalation. A good question contains the context, the options, the recommendation, and the consequence of each choice.&lt;/p&gt;

&lt;p&gt;For example: "The ticket has no story points. I estimate it as five because it touches the policy form and validation flow, but the description does not say whether the API changes too. Should I use five, or wait for clarification?"&lt;/p&gt;

&lt;p&gt;That is not the agent giving up. It has done the investigation and isolated the unresolved decision. The human only needs to resolve the part that genuinely depends on intent.&lt;/p&gt;

&lt;p&gt;Poor escalation pushes the whole problem back: "Please clarify the task." Good escalation makes the remaining uncertainty small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change the boundary when the workflow matures
&lt;/h2&gt;

&lt;p&gt;The right boundary is not permanent. Early in a workflow, the agent may need approval to open a pull request. After enough successful runs, drafting the pull request can become automatic while merging remains protected.&lt;/p&gt;

&lt;p&gt;This is how I would increase autonomy: one decision at a time, based on evidence. Do not grant a broad permission because the demo looked good. Move one reversible action across the boundary, observe the results, and keep the approval where failures would be costly.&lt;/p&gt;

&lt;p&gt;A rule that nobody revisits becomes a superstition. A rule that is changed after evidence becomes an operating model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;People often ask whether agents are ready to work autonomously. That question is too blunt to be useful. Ready to do what, with which permissions, under which stop conditions, and with what recovery path?&lt;/p&gt;

&lt;p&gt;A good agent can act independently for a long stretch and still be designed around human judgment. The human does not need to approve every keystroke. The human needs to own the decisions that carry intent, irreversible consequences, or unclear authority.&lt;/p&gt;

&lt;p&gt;Look at the last time your agent asked a question. Was it asking about the right thing? Now look at the last time it guessed. That comparison will show you where the boundary actually belongs.&lt;/p&gt;

&lt;p&gt;Once the boundary is clear, the next challenge is continuity. An agent that learns a rule in one session but forgets it in the next is not autonomous. It is repeatedly starting over.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>Tools Are Part of the Agent's Intelligence</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Thu, 20 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/tools-are-part-of-the-agents-intelligence-mpj</link>
      <guid>https://dev.to/maximusft/tools-are-part-of-the-agents-intelligence-mpj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffa0hk6tcu221gzk6f6q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffa0hk6tcu221gzk6f6q.png" alt="Tools Are Part of the Agent's Intelligence" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;People talk about giving an agent more tools as if they were adding buttons to a control panel. Add Jira. Add Confluence. Add GitHub. Add Postman. Now the agent is more capable.&lt;/p&gt;

&lt;p&gt;Sometimes it is. Sometimes you have just built a larger menu of ambiguously named actions and made it harder for the agent to choose correctly.&lt;/p&gt;

&lt;p&gt;A tool is not an API wrapper from the agent's point of view. It is a piece of reasoning infrastructure. Its name, description, parameters, return value, permissions, and failure behavior all shape the decisions the agent can make.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A tool is part of an agent's intelligence because it determines what the agent can know and how safely it can act.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The hidden cost of a bad tool
&lt;/h2&gt;

&lt;p&gt;Imagine a tool called update_issue. It accepts an issue ID and a free-form payload. Technically, it can do everything: change the status, assign an owner, move the issue to a sprint, add a comment, or alter an estimate.&lt;/p&gt;

&lt;p&gt;It is also asking the agent to reconstruct your entire process from one dangerous verb.&lt;/p&gt;

&lt;p&gt;A better interface exposes the meaningful actions separately. Assign the issue. Move it to In Progress. Add it to the current sprint. Set story points. Each action has a narrower purpose and a smaller set of valid inputs. The agent has fewer ways to misunderstand the operation.&lt;/p&gt;

&lt;p&gt;This is the same reason good application code uses domain functions instead of one universal mutate function. The boundary carries meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Descriptions are executable design
&lt;/h2&gt;

&lt;p&gt;Humans can fill in missing details from experience. Agents cannot reliably do that, especially when several interpretations are plausible. A tool description should answer the questions an agent will have at the moment of choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does this action change?&lt;/li&gt;
&lt;li&gt;What does it leave untouched?&lt;/li&gt;
&lt;li&gt;When should it be used?&lt;/li&gt;
&lt;li&gt;What does success look like?&lt;/li&gt;
&lt;li&gt;What happens when the requested state is already true?&lt;/li&gt;
&lt;li&gt;Which actions are forbidden or require confirmation?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A description that says "updates a ticket" is almost empty. A description that says "moves an assigned ticket into the current sprint without changing its estimate" gives the agent an actual contract.&lt;/p&gt;

&lt;p&gt;The return value matters just as much. Returning "done" hides the new state. Returning the issue ID, status, assignee, sprint, and a clear error gives the next step something reliable to reason from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer tools that make wrong actions difficult
&lt;/h2&gt;

&lt;p&gt;The best tools do not merely explain the right behavior. They make common wrong behavior hard to express. This is the poka-yoke idea applied to agent interfaces: shape the tool so a mistake becomes impossible or obvious.&lt;/p&gt;

&lt;p&gt;A tool that opens a pull request should not silently choose the base branch. A deployment tool should expose the target environment explicitly. A production mutation should not look identical to a staging mutation with one invisible string changed.&lt;/p&gt;

&lt;p&gt;Narrow parameters help. Typed enums help. Read-only previews help. Separate approval actions help even more. The agent should be able to inspect what it is about to do before it commits to the irreversible part.&lt;/p&gt;

&lt;p&gt;That is not excessive ceremony. It is how you move risk out of the model's free-form language and into a boundary you can test.&lt;/p&gt;

&lt;h2&gt;
  
  
  One excellent tool beats five overlapping ones
&lt;/h2&gt;

&lt;p&gt;Tool count is another place where agent design starts to resemble multi-agent design. More looks more capable. In practice, overlapping tools create routing uncertainty. The agent spends tokens deciding which nearly identical action to call, then has to recover when the chosen tool has subtly different semantics.&lt;/p&gt;

&lt;p&gt;I would rather expose one clear tool for a meaningful operation than five endpoints that reflect how the backend happens to be organized. The agent does not need to see your internal service decomposition. It needs a stable action model.&lt;/p&gt;

&lt;p&gt;This is also where MCP servers can either help or hurt. Connecting Jira, Confluence, docs, and Postman gives an agent access to the world. But access alone is not context. Each server still needs coherent names, useful descriptions, predictable errors, and permissions that match the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design tools from the failure backwards
&lt;/h2&gt;

&lt;p&gt;Start with a real mistake. The agent branched from stale main. It guessed a story-point estimate. It used the wrong API endpoint. It opened a pull request without the required context.&lt;/p&gt;

&lt;p&gt;Now ask which tool boundary allowed the mistake. Was the information missing? Was the action too broad? Could the tool have returned the state the agent needed? Did the agent have a safe way to ask for confirmation?&lt;/p&gt;

&lt;p&gt;Fix the boundary, not just the prompt. A prompt correction helps one context. A better tool helps every future context that reaches the same decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;We usually describe tools as the agent's hands. I think they are closer to its senses and its joints. They determine what it can perceive, which movements are available, and where the force of a bad decision lands.&lt;/p&gt;

&lt;p&gt;A strong model with weak tools is a talented engineer working through a broken dashboard. A modest model with clear, narrow, truthful tools can often do much better because the environment removes whole categories of error.&lt;/p&gt;

&lt;p&gt;Look at the most important tool your agent uses today. Can it explain its contract? Can it show the state it changed? Can it make a dangerous action explicit? If not, that tool is not finished just because the API call works.&lt;/p&gt;

&lt;p&gt;The next boundary is not a tool at all. It is the moment where the agent should stop, surface uncertainty, and let a person decide.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>The Context Is the Product</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/the-context-is-the-product-2ja4</link>
      <guid>https://dev.to/maximusft/the-context-is-the-product-2ja4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hjqxdkicjywiv53xfzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hjqxdkicjywiv53xfzv.png" alt="The Context Is the Product" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A model can be brilliant and still be useless in your project.&lt;/p&gt;

&lt;p&gt;Give it a ticket with no surrounding context, a repository with no map, and a handful of tools with vague descriptions. It will produce something plausible. That is exactly the problem. Plausible is not the same as correct.&lt;/p&gt;

&lt;p&gt;I used to think of context as the thing I supplied to an agent before the real work began. The prompt, the files, the issue. Now I think that is backwards. Context is not the preamble to the work. Context is the product I am building around the model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An agent is only as good as the world it can see before it makes a decision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The model is not the whole system
&lt;/h2&gt;

&lt;p&gt;The model gets most of the attention because it is the visible intelligence. But an agent run is closer to a small operating system than a chat completion. It has a goal, a working memory, tools, permissions, project rules, and a history of decisions. Change any of those and the same model behaves differently.&lt;/p&gt;

&lt;p&gt;That is why comparing agents by model name alone is incomplete. Two teams can use the same model and get completely different results. One gives it the repository map, the conventions, the API documentation, and a clear stop condition. The other gives it a ticket and hopes the model guesses the rest.&lt;/p&gt;

&lt;p&gt;The second team is not using a weaker model. It is giving the model a weaker world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context has layers
&lt;/h2&gt;

&lt;p&gt;The first layer is the task: what needs to change and why. A ticket that says "fix the payment flow" is not a task. It is a label. A useful task includes the observed behavior, the expected behavior, the boundary of the change, and the check that proves it is done.&lt;/p&gt;

&lt;p&gt;The second layer is the project: where code lives, which package owns the behavior, how data moves, and which commands validate a change. This is the map that stops an agent from editing the first file whose name looks relevant.&lt;/p&gt;

&lt;p&gt;The third layer is convention. Naming, branching, testing, commit messages, API boundaries, and the moments where the agent must ask instead of guessing. Conventions turn a technically possible action into an acceptable one.&lt;/p&gt;

&lt;p&gt;The fourth layer is history. Why is this abstraction here? Which migration is still in progress? Which shortcut is deliberate? Without that history, an agent sees an odd line and helpfully removes the reason it exists.&lt;/p&gt;

&lt;p&gt;The last layer is access. A tool is part of context because it changes what the agent can know and do. Read-only access to Jira creates one kind of workflow. The ability to update a ticket and open a pull request creates another.&lt;/p&gt;

&lt;h2&gt;
  
  
  More context is not always better
&lt;/h2&gt;

&lt;p&gt;There is a trap here. If missing context causes mistakes, the natural reaction is to attach everything. Every document, every repository, every old discussion. That creates a larger transcript, not necessarily a clearer world.&lt;/p&gt;

&lt;p&gt;Good context is selected context. It answers the next decision the agent has to make. A small project map is better than a directory dump. A short rule about API ownership is better than a hundred pages of loosely related documentation. A current decision record is better than three contradictory conversations.&lt;/p&gt;

&lt;p&gt;The test is simple: can the agent tell which information is authoritative? If it cannot, more context increases the number of ways it can be confidently wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the context deliberately
&lt;/h2&gt;

&lt;p&gt;I would start with four durable pieces: a project map, a set of working rules, a decision log, and a list of available tools. Keep each one short enough to remain current. A stale instruction is worse than no instruction because it makes the wrong action look official.&lt;/p&gt;

&lt;p&gt;Then connect the context to the workflow. When a task arrives, the agent should be able to discover the relevant feature, read the owning documentation, inspect recent decisions, make the smallest change, and run the expected check. That sequence is more valuable than a clever initial prompt.&lt;/p&gt;

&lt;p&gt;This is also why the investment compounds. Every correction that becomes a durable rule improves the next run. Every tool that removes a guess improves the next decision. Every obsolete instruction you delete removes one false path.&lt;/p&gt;

&lt;p&gt;The agent is not becoming magically smarter. The environment around it is becoming more legible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;The exciting part of agents is usually the model. The durable advantage is the context architecture around it. Models will change. A clear project map, coherent rules, useful tools, and visible decisions will keep paying rent.&lt;/p&gt;

&lt;p&gt;So when an agent keeps making the same kind of mistake, I do not begin by asking whether I need a stronger model. I ask what part of the world was invisible, ambiguous, or contradictory at the moment it decided.&lt;/p&gt;

&lt;p&gt;That question moves the work upstream. You stop polishing prompts and start designing the environment in which decisions happen. The next question is obvious: if tools define what the agent can see and change, tool design is part of the agent's intelligence too.&lt;/p&gt;

&lt;p&gt;Take one recurring correction from your last agent session and ask what context would have prevented it. Write that context where the next session can actually find it. That is the beginning of the product.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>Guardrails for a Runaway Agent</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/guardrails-for-a-runaway-agent-8h5</link>
      <guid>https://dev.to/maximusft/guardrails-for-a-runaway-agent-8h5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbahuquz44tgvkojjeylp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbahuquz44tgvkojjeylp.png" alt="Guardrails for a Runaway Agent" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything in this cluster has circled the same failure: an agent that runs longer, spends more, and drifts further than you intended, with nothing to stop it. The &lt;a href="https://ma-x.im/blog/agent-playbook-you-dont-need-multi-agents" rel="noopener noreferrer"&gt;multi-agent orchestra&lt;/a&gt; fractures context, the &lt;a href="https://ma-x.im/blog/agent-playbook-token-bill" rel="noopener noreferrer"&gt;token bill&lt;/a&gt; balloons, the &lt;a href="https://ma-x.im/blog/agent-playbook-test-on-change" rel="noopener noreferrer"&gt;scheduled loop&lt;/a&gt; burns all night. Underneath all of them is one uncomfortable property, and Anthropic name it directly: agents are stateful, and their errors &lt;em&gt;compound&lt;/em&gt;. One bad step doesn't just produce one bad result — it changes the state the next step reasons from, so the agent quietly walks off in a completely different direction and has no idea it's lost.&lt;/p&gt;

&lt;p&gt;That's the thing traditional software doesn't really do. A bug usually breaks a feature and stops. An agent's bug can send it on a confident, expensive journey to nowhere. So the design question isn't "how do I make the agent never fail?" — you can't. It's "when it fails, how do I make sure it fails &lt;em&gt;small&lt;/em&gt;?"&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don't grant an agent autonomy by trusting it. You grant it autonomy by bounding what it can do when you're wrong to trust it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Cap the loop
&lt;/h2&gt;

&lt;p&gt;The first guardrail is the dumbest one and the most important: a hard ceiling on how many steps or iterations a run can take. Anthropic build exactly this into their agents — a maximum number of iterations as a stopping condition, so a task that can't converge &lt;em&gt;terminates&lt;/em&gt; instead of spinning. This is the single line that turns the overnight-burn story into a non-event: the agent tries twenty times, hits the cap, stops, and tells you it's stuck. "Burned all night" becomes "gave up before breakfast."&lt;/p&gt;

&lt;h2&gt;
  
  
  Budget the spend
&lt;/h2&gt;

&lt;p&gt;The step cap bounds &lt;em&gt;count&lt;/em&gt;; a spend cap bounds &lt;em&gt;cost&lt;/em&gt; directly. Give a run a token or dollar budget and a circuit breaker that trips when it's exceeded. Even if you got the iteration ceiling wrong, the budget is a second, independent wall between a stuck agent and a catastrophic invoice. Two different limits guarding the same failure is not redundancy — it's the point. The whole philosophy here is that any single guardrail might be the one you misconfigured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notice when it stops making progress
&lt;/h2&gt;

&lt;p&gt;A cap stops an agent eventually; progress-detection stops it &lt;em&gt;sooner&lt;/em&gt;. If the agent takes the same action, gets the same result, and tries again — that's not work, that's a loop, and you can detect it. Repeated identical tool calls, the same error over and over, no change in state across steps: these are all signals that the run has stopped advancing and should be cut short. It's the difference between letting it exhaust twenty iterations and killing it at the third pointless repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a human at the sharp edges
&lt;/h2&gt;

&lt;p&gt;Not every action deserves the same freedom. Reading a ticket, running a test, opening a draft PR — low consequence, let it run. Deleting data, pushing to a shared branch, spending real money, touching production — these are where autonomy should pause and check with a person. This isn't timidity; it's the &lt;a href="https://ma-x.im/blog/agent-playbook-do-it-as-usual" rel="noopener noreferrer"&gt;same negotiated boundary&lt;/a&gt; that makes "do it as usual" safe to run unattended. The agent owns the reversible actions and stops at the irreversible ones. You decide where that line sits per action, deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shrink the blast radius
&lt;/h2&gt;

&lt;p&gt;The guardrails above assume the agent &lt;em&gt;tries&lt;/em&gt; something bad; sandboxing assumes it &lt;em&gt;succeeds&lt;/em&gt; and makes that survivable. Least privilege is the whole idea: an agent that physically cannot reach production can't take it down, no matter how confused it gets. Run in an environment where the worst thing it can do is still recoverable — a scratch branch, a staging database, scoped credentials. Then a runaway is an annoyance, not an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  See why it ran away
&lt;/h2&gt;

&lt;p&gt;You can't tighten a guardrail you can't see through. Anthropic lean hard on tracing agent decisions for exactly this reason — when an agent "can't find obvious information" or spirals, the only way to fix it systematically is to look at what it actually did, step by step. Observability isn't a nice-to-have bolted on at the end; it's how you learn which guardrail was missing. Every runaway you can inspect becomes a rule you can add.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;There's a temptation to read all of this as distrust — as clipping the agent's wings. I see it the opposite way. Guardrails are what let me hand an agent &lt;em&gt;more&lt;/em&gt; autonomy, not less, because I know the failure modes are bounded. The reason I can say "do it as usual" and walk away is precisely that a stuck run caps out, a runaway can't reach anything that matters, and I can see afterward what went wrong. The bounds are what make the trust affordable.&lt;/p&gt;

&lt;p&gt;An agent without guardrails isn't a bold, high-trust setup. It's an unbounded process with a credit card and access to your systems, and the only thing standing between it and a bad night is luck. Design the blast radius first, and autonomy stops being a gamble. That's the through-line of this whole cluster: the goal was never a more impressive agent. It was a &lt;em&gt;sane&lt;/em&gt; one — one you can actually let run, because you decided in advance how it's allowed to fail.&lt;/p&gt;

&lt;p&gt;Take one agent you run unattended and write down what happens if it gets stuck right now: does it stop on its own, and what can it reach while it's confused? If either answer is uncomfortable, you've found your next guardrail. Tell me which one — I'd bet it's the iteration cap, because it almost always is.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
  </channel>
</rss>
