<?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: Diven Rastdus</title>
    <description>The latest articles on DEV Community by Diven Rastdus (@astraedus).</description>
    <link>https://dev.to/astraedus</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%2F3807319%2F88334a5b-4b5d-412a-a196-402f05bca721.png</url>
      <title>DEV Community: Diven Rastdus</title>
      <link>https://dev.to/astraedus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/astraedus"/>
    <language>en</language>
    <item>
      <title>5 AI Agent Features That Define 2026 (And 2 I'm Still Waiting For)</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Wed, 19 Aug 2026 10:12:42 +0000</pubDate>
      <link>https://dev.to/astraedus/5-ai-agent-features-that-define-2026-and-2-im-still-waiting-for-2bef</link>
      <guid>https://dev.to/astraedus/5-ai-agent-features-that-define-2026-and-2-im-still-waiting-for-2bef</guid>
      <description>&lt;p&gt;The five features that define AI agents in 2026 are a shared tool standard (MCP), prompt caching, computer use, background subagents, and agent-to-agent messaging. None of them are the model itself. They are the plumbing that turns a clever text generator into something that does actual work.&lt;/p&gt;

&lt;p&gt;I run agents in production every day. For most of 2024 an "agent" meant a model stuck in a &lt;code&gt;while&lt;/code&gt; loop, and it broke the moment it touched the real world. In 2026 that stopped being true, and it had almost nothing to do with the models getting smarter. It was the layers underneath.&lt;/p&gt;

&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%2Fiextpcxqt4p5gr1knvwj.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%2Fiextpcxqt4p5gr1knvwj.png" alt="The 2026 AI agent stack: foundation model, then MCP, prompt caching, computer use, subagents, and A2A, with durable memory and long-horizon autonomy still missing" width="800" height="1086"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the five that changed how I build, and the two I'm still waiting on.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. MCP became the universal tool layer
&lt;/h2&gt;

&lt;p&gt;MCP turned connecting an agent to a tool into one open standard, instead of a bespoke integration per vendor. That's the single biggest shift of 2026. The Model Context Protocol (MCP), which Anthropic released in November 2024, does for agent tools what USB did for peripherals: write the tool once, and any model can call it.&lt;/p&gt;

&lt;p&gt;The adoption is not a rumor. OpenAI adopted MCP in March 2025. In December 2025 Anthropic donated it to the Agentic AI Foundation, a new fund under the Linux Foundation, so it's now vendor-neutral infrastructure rather than one company's project. A tool server is tiny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return today&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s forecast for a city.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That server now works with Claude, ChatGPT, and anything else that speaks MCP. Before this, I wrote the same tool three times. Now I write it once and forget which model is on the other end.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Prompt caching made the agent loop affordable
&lt;/h2&gt;

&lt;p&gt;Agents got cheap enough to run in loops because providers now let you reuse a cached prompt prefix for roughly a tenth of the price. An agent re-sends the same giant system prompt (tools, instructions, context) on every single turn. Paying full price for that on turn 40 was the hidden tax that killed long-running agents.&lt;/p&gt;

&lt;p&gt;By 2026 all three major providers discount cached input by up to 90%. On Anthropic you mark the stable part of the prompt once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BIG_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# tools + instructions + docs
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;do the next step&lt;/span&gt;&lt;span class="sh"&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;You pay a small write premium the first time (1.25x on the 5-minute tier), then every later turn reads that prefix at 0.1x. A 40-turn agent run that used to feel reckless now costs pocket change. This is the feature nobody puts on a launch slide, and it's the one that quietly unlocked everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Computer use turned agents into hands, not just mouths
&lt;/h2&gt;

&lt;p&gt;Agents can now see a screen and operate it directly, so any app with a UI is reachable even when it has no API. Claude's computer-use launch in October 2024 was the inflection point. Anthropic's own benchmarks show the jump: computer-use task completion on OSWorld climbed from about 15% at launch to over 70% in 2026.&lt;/p&gt;

&lt;p&gt;You give the model a screen and a set of physical actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;computer_20250124&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# use the current tool version from the docs
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;computer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;display_width_px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;display_height_px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="c1"&gt;# The model replies with actions: screenshot, click(x, y), type("...").
# You execute each one, send back a fresh screenshot, and repeat.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use this to drive a real browser with our own logins. It reads the page, decides what to click, and adapts when the layout moves. That's the difference from old RPA scripts, which shattered the first time a button shifted ten pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Background subagents made parallelism the default
&lt;/h2&gt;

&lt;p&gt;The best agent systems in 2026 stopped being one model in one loop and became many agents running at once, in the background. Instead of a human running tasks one after another, you fan out a fleet, each subagent with its own context window and its own tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# each subagent: isolated context, its own tools, its own model
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&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="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both Claude Code and Codex shipped this: Codex spins up parallel cloud sandboxes and returns pull requests, and background sessions now survive a closed laptop. I lean on this constantly. A review that would take one agent an hour of sequential reading becomes ten agents reading in parallel, and I only hold the synthesis in context. Parallelism is free in a digital medium, and 2026 is the year the tools finally made it easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A2A let agents talk to other agents
&lt;/h2&gt;

&lt;p&gt;A2A is a standard for agents to discover and call each other across vendors, the way MCP standardized tools. It's the newest layer in the 2026 stack. Google's Agent2Agent (A2A) protocol, announced in April 2025 and donated to the Linux Foundation, reached a stable v1.0 in early 2026 with more than 150 organizations backing it.&lt;/p&gt;

&lt;p&gt;An agent advertises itself with an Agent Card, a small manifest other agents can read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Answers invoice and payment questions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.acme.com/a2a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"streaming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skills"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lookup_invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Look up an invoice"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP connects an agent to tools. A2A connects an agent to other agents. It's early, but the shape of a real multi-agent internet is now visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2 I'm still waiting for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Durable native memory.&lt;/strong&gt; Every serious agent I run still fakes memory with a bolt-on: a vector database, or a folder of markdown files I re-inject each session. It works, but it's scaffolding I built by hand. No provider ships memory that survives across sessions and actually generalizes what it learned. Until that's native, "my agent remembers you" is a feature you engineer, not one you turn on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trustworthy long-horizon autonomy.&lt;/strong&gt; Agents are brilliant for twenty steps and drift on step two hundred. Errors compound, the plan wanders, and the only reliable fix is a human checking the work. I still verify every meaningful agent output against a real source of truth, never the agent's own "done." Real autonomy means the agent catches its own drift. We are not there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The model got the headlines in 2026. The progress that changed my day-to-day was the stack around it: a tool standard, cheap loops, real hands, parallel execution, and agents that talk to each other. You don't need to adopt all five. Pick the one layer that removes your biggest friction (usually MCP or caching) and add it to whatever you're already building. That's where the real gains are hiding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox -&amp;gt; &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
      <category>llm</category>
    </item>
    <item>
      <title>5 TypeScript Features That Define 2026 (And 2 I'm Still Waiting For)</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Wed, 05 Aug 2026 10:22:33 +0000</pubDate>
      <link>https://dev.to/astraedus/5-typescript-features-that-define-2026-and-2-im-still-waiting-for-565f</link>
      <guid>https://dev.to/astraedus/5-typescript-features-that-define-2026-and-2-im-still-waiting-for-565f</guid>
      <description>&lt;p&gt;The biggest TypeScript change of 2026 isn't a new type. It's a new engine. The compiler got rewritten in Go and now type-checks about ten times faster, and Node runs your &lt;code&gt;.ts&lt;/code&gt; files with no build step at all. A few of these landed across 5.8 and 5.9 during 2025, but together they're the toolchain I'm actually standing on this year. Here are the five I reach for across our projects, and the two I keep waiting on.&lt;/p&gt;

&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%2F6edx6ea14vrh3wt7fafq.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%2F6edx6ea14vrh3wt7fafq.png" alt="Why TypeScript 7 type-checks about 10x faster: porting the compiler from TypeScript-on-Node to a native Go binary drops a full VS Code type-check from 77.8s to 7.5s" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The compiler went native, and it's roughly 10x faster
&lt;/h2&gt;

&lt;p&gt;The headline feature of TypeScript 7 is that the compiler is now a native binary written in Go. Microsoft measured a full type-check of the VS Code codebase dropping from 77.8 seconds to 7.5 seconds. That's a 10.4x cut on real code, not a toy benchmark.&lt;/p&gt;

&lt;p&gt;The project codename is Corsa, and Anders Hejlsberg (TypeScript's lead architect) is running it. The important part: there's no new type system and no new syntax. The team took the existing checker and ported it, keeping the logic structurally identical. So your types behave the same. The wait just gets shorter.&lt;/p&gt;

&lt;p&gt;You can try it today from the preview package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @typescript/native-preview
npx tsgo &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it on one of our Expo apps. A clean &lt;code&gt;tsc --noEmit&lt;/code&gt; took 4.2 seconds; the same check on &lt;code&gt;tsgo&lt;/code&gt; took 0.8 seconds. That's about 5x on a small codebase, and the gap only widens on big ones, which is where the VS Code 10.4x comes from. Expect one bump on the way in: &lt;code&gt;tsgo&lt;/code&gt; immediately rejected our &lt;code&gt;baseUrl&lt;/code&gt; option with &lt;code&gt;error TS5102&lt;/code&gt;, because TypeScript 7 removed it. Paths resolve relative to the config file now, so I deleted one line and it passed. From the release candidate onward the native compiler folds back into the normal &lt;code&gt;typescript&lt;/code&gt; package and the &lt;code&gt;tsc&lt;/code&gt; command, so most projects bump a version and change little else. The one caveat before you migrate: TypeScript 7.0 has no stable programmatic API yet, which is why some tools still lag (more on that below).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. You can run TypeScript with no build step
&lt;/h2&gt;

&lt;p&gt;TypeScript 5.8 added &lt;code&gt;--erasableSyntaxOnly&lt;/code&gt;, which pairs with Node's native type stripping so you can run a &lt;code&gt;.ts&lt;/code&gt; file directly. No transpile, no bundler, no &lt;code&gt;dist&lt;/code&gt; folder for local scripts.&lt;/p&gt;

&lt;p&gt;Node strips the type annotations and runs the rest. That works for almost everything, but a few TypeScript features emit real runtime code and can't be stripped: enums, namespaces with runtime bodies, and constructor parameter properties. The flag catches those at author time so you never ship code Node can't run:&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="c1"&gt;// Both of these now error under erasableSyntaxOnly:&lt;/span&gt;
&lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;Color&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Red&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Green&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;          &lt;span class="c1"&gt;// enums compile to a runtime object&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;id&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="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;// parameter property emits an assignment&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn it on in your config and run the file straight from Node (22.18 and up, on by default in Node 24):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"erasableSyntaxOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node server.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap: type stripping deletes types and runs the code without checking them. A type error won't stop the program. You still need &lt;code&gt;tsc --noEmit&lt;/code&gt; in CI to actually catch mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;import defer&lt;/code&gt; lets you lazy-load a module
&lt;/h2&gt;

&lt;p&gt;TypeScript 5.9 shipped support for &lt;code&gt;import defer&lt;/code&gt;, a Stage 3 ECMAScript proposal that delays evaluating a module until you first touch one of its exports. It's a clean win for expensive dependencies you don't always use.&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;import&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;heavyAnalytics&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./heavy-analytics.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trackFirstOpen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// heavy-analytics.js runs only when this line executes,&lt;/span&gt;
  &lt;span class="c1"&gt;// not when the file is imported at startup&lt;/span&gt;
  &lt;span class="nx"&gt;heavyAnalytics&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="s2"&gt;app_opened&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One rule to remember: only namespace imports (&lt;code&gt;import defer * as x&lt;/code&gt;) are allowed. Named and default imports aren't, because the whole point is to avoid evaluating the module up front. I use this for analytics and PDF generation, the code paths most users never hit on a cold start.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;--module node20&lt;/code&gt; gives you a target that stops moving
&lt;/h2&gt;

&lt;p&gt;TypeScript 5.9 added &lt;code&gt;--module node20&lt;/code&gt;, a stable option that models Node.js 20 behavior and implies &lt;code&gt;--target es2023&lt;/code&gt;. The value here is predictability.&lt;/p&gt;

&lt;p&gt;The older &lt;code&gt;nodenext&lt;/code&gt; setting tracks whatever the latest Node does, so its behavior shifts under you as Node evolves. &lt;code&gt;node20&lt;/code&gt; pins to a known version and stays put, which is exactly what you want in a &lt;code&gt;tsconfig.json&lt;/code&gt; you won't touch for a year:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node20"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// stable, and implies "target": "es2023"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small feature, but it removes a class of "it worked last month" surprises in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Better defaults and hovers make the editor pull its weight
&lt;/h2&gt;

&lt;p&gt;TypeScript 5.9 reworked &lt;code&gt;tsc --init&lt;/code&gt; to emit a lean config instead of a hundred commented-out lines, and it made editor hovers far more useful. This is the quality-of-life bucket, and it matters more than it sounds.&lt;/p&gt;

&lt;p&gt;The new &lt;code&gt;tsc --init&lt;/code&gt; writes a short, readable file instead of a wall of comments, close to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"esnext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodenext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, DOM type definitions now carry summary descriptions pulled from MDN, so hovering &lt;code&gt;element.scrollIntoView&lt;/code&gt; explains what it does without a trip to the browser. Hovers also got expandable &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;-&lt;/code&gt; buttons to drill into a deep type instead of squinting at a truncated blob.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2 I'm still waiting for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A stable compiler API on the native build.&lt;/strong&gt; Every one of our repos runs typescript-eslint, and template type-checking for Vue, Svelte, and Astro all lean on TypeScript's programmatic API. TypeScript 7.0 ships without a stable one, so those tools can't run on the native compiler yet. The API is targeted for 7.1, several months out. Until then you get the fast &lt;code&gt;tsc&lt;/code&gt;, but your lint and framework checks still ride the old engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nominal (opaque) types.&lt;/strong&gt; TypeScript is still structural, so two &lt;code&gt;string&lt;/code&gt; aliases are interchangeable no matter what you name them. To fake a real distinct type, we hand-roll brands:&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;UserId&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="s2"&gt;UserId&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;PostId&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="s2"&gt;PostId&lt;/span&gt;&lt;span class="dl"&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;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// getUser(somePostId as PostId)  // the brand trick works, but it's a trick&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, and it has saved us from passing a post id where a user id belongs. But a &lt;code&gt;__brand&lt;/code&gt; phantom field is a workaround for a language feature that should exist. Give me a real &lt;code&gt;nominal&lt;/code&gt; keyword.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The theme for TypeScript in 2026 is speed and less ceremony, not more type gymnastics. The native compiler makes big codebases feel small again, no-build-step &lt;code&gt;.ts&lt;/code&gt; kills a whole tooling layer for scripts, and &lt;code&gt;import defer&lt;/code&gt; trims your startup cost. Upgrade when you can, but keep &lt;code&gt;tsc --noEmit&lt;/code&gt; in CI as your real safety net, because the fastest features here are the ones that skip type-checking entirely. Fast is only good when it's also correct.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox → &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Give Your React Native App a Memory That Actually Works</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Mon, 03 Aug 2026 10:18:45 +0000</pubDate>
      <link>https://dev.to/astraedus/how-to-give-your-react-native-app-a-memory-that-actually-works-2a2l</link>
      <guid>https://dev.to/astraedus/how-to-give-your-react-native-app-a-memory-that-actually-works-2a2l</guid>
      <description>&lt;p&gt;A React Native app has three separate kinds of memory, and almost every "my data disappeared" bug comes from storing a value in the wrong one. Once you can name the three tiers and match each piece of data to the right one, "remembering" stops being a mystery and becomes a routing decision.&lt;/p&gt;

&lt;p&gt;Here's the problem you'll hit. A user fills out onboarding, sets dark mode, adds three entries, then force-quits the app. They reopen it and everything's gone. Or the softer version: the app launches in light mode for half a second, flashes white, then snaps to the dark theme they actually chose. Both are the same bug wearing different clothes. Something lived in the wrong tier of memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your app has three kinds of memory
&lt;/h2&gt;

&lt;p&gt;A React Native app stores data in three local tiers, and each one survives a different amount of destruction. Above them sits the server, which is the only thing that survives a reinstall.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-memory state&lt;/strong&gt; (&lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useReducer&lt;/code&gt;, Zustand held in RAM). Fast, but it survives nothing. A reload, a background-kill, or a crash wipes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key-value storage&lt;/strong&gt; (MMKV, AsyncStorage, expo-secure-store). Small values that survive a restart. Themes, flags, tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured storage&lt;/strong&gt; (expo-sqlite, WatermelonDB). Rows you can query, sort, and read offline. Notes, entries, a synced list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server (Supabase or your own API) is the source of truth. Every local tier is a cache of it.&lt;/p&gt;

&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%2F5ib8fcbisg3ldglz6cb0.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%2F5ib8fcbisg3ldglz6cb0.png" alt="The three tiers of React Native memory" width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Match each value to the right tier
&lt;/h2&gt;

&lt;p&gt;The right tier for a value comes down to two questions: how much destruction it must survive, and how you'll read it back. A single flag reads differently than a list you sort.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you are storing&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Store to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth token, API keys&lt;/td&gt;
&lt;td&gt;secrets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expo-secure-store&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme, onboarding-done, feature flags&lt;/td&gt;
&lt;td&gt;small key-value&lt;/td&gt;
&lt;td&gt;MMKV / AsyncStorage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notes, entries, a list you query&lt;/td&gt;
&lt;td&gt;structured records&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expo-sqlite&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The canonical copy shared across devices&lt;/td&gt;
&lt;td&gt;source of truth&lt;/td&gt;
&lt;td&gt;your server, cached locally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One security note that catches people. AsyncStorage and MMKV aren't encrypted by default, so an auth token doesn't belong there. Secrets go in &lt;code&gt;expo-secure-store&lt;/code&gt;, which is backed by the iOS Keychain and Android Keystore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your data disappears: AsyncStorage is async
&lt;/h2&gt;

&lt;p&gt;The most common "data loss" bug isn't lost data at all. It's reading storage a beat &lt;em&gt;after&lt;/em&gt; your UI has already rendered with a default value.&lt;/p&gt;

&lt;p&gt;AsyncStorage returns a promise. Your component renders once with its initial state, then the saved value arrives and triggers a second render. The user sees the gap.&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;useThemePref&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;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTheme&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// default paints first&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="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;saved&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// real value lands after first paint&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="nx"&gt;theme&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 user picked dark mode last week. On launch they still see a white flash, then it corrects. The data was never lost. You just read it too late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make key-value memory synchronous with MMKV
&lt;/h2&gt;

&lt;p&gt;MMKV reads and writes synchronously, so the value is already there on the very first render. No promise, no await, no flash. Pair it with Zustand's persist middleware and your store rehydrates itself.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MMKV&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native-mmkv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createJSONStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StateStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zustand/middleware&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;storage&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;MMKV&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;mmkvStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StateStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&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;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&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="na"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&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;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&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;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usePrefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&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="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&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;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;theme&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prefs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createJSONStorage&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;mmkvStorage&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the read is synchronous, &lt;code&gt;usePrefs.getState().theme&lt;/code&gt; is correct on the first paint. The white flash from the AsyncStorage version is gone, and you didn't change a single component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate the first render on hydration
&lt;/h2&gt;

&lt;p&gt;When a tier is genuinely async (SQLite, a network cache), the cleanest fix is to not render the app until its memory has loaded. Hold a spinner until the first read resolves, then mount the tree with real data.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ActivityIndicator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSQLiteContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expo-sqlite&lt;/span&gt;&lt;span class="dl"&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;Root&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;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSQLiteContext&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;prefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPrefs&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="nx"&gt;Prefs&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="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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select key, value from settings&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rows&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;setPrefs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rowsToObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt; &lt;span class="c1"&gt;// memory is now warm&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;db&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;prefs&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="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&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="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActivityIndicator&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="nc"&gt;View&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="na"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;prefs&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;This is exactly what a settings provider backed by expo-sqlite does in our own apps. It shows a spinner while the first query runs, seeds its context with the saved rows, and only then renders the screens. The user never sees a default value flicker into the real one, because the real one was loaded before anything painted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Storing data in React Native is a routing problem, not a library problem. You're not choosing between AsyncStorage and SQLite in the abstract. You're asking, for one specific value, how much it must survive and how you'll read it back. Answer those two questions and the tier picks itself.&lt;/p&gt;

&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%2Fep6rmnwn3koxhqq72tnk.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%2Fep6rmnwn3koxhqq72tnk.png" alt="Where should this piece of data live?" width="800" height="777"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three rules carry most of the weight. Put each value in the lowest tier that meets its durability and access needs. Never let an async read paint a default first, either make the read synchronous or gate the render on it. And keep the server as the source of truth, with every local tier treated as a cache you can rebuild. Do that and your app remembers exactly what the user expects, every time they come back.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox → &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Practice App Because Every "Duolingo for Music" Kept Failing Me</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Sun, 02 Aug 2026 13:34:22 +0000</pubDate>
      <link>https://dev.to/astraedus/i-built-a-practice-app-because-every-duolingo-for-music-kept-failing-me-4gde</link>
      <guid>https://dev.to/astraedus/i-built-a-practice-app-because-every-duolingo-for-music-kept-failing-me-4gde</guid>
      <description>&lt;p&gt;I've lost track of how many "Duolingo for piano" apps I've downloaded and deleted. Simply Piano, Yousician, a few others I don't remember the names of. Every one of them followed the same shape: score how close your playing matched a reference MIDI file, throw a badge at you, ask for a monthly subscription. None of them ever told me why a passage was hard, or when I was actually ready to move past it. They graded me. They didn't teach me.&lt;/p&gt;

&lt;p&gt;So I built the app I actually wanted and put it up for anyone else with the same problem. It's called &lt;a href="https://music.raeduslabs.com" rel="noopener noreferrer"&gt;Music Practice&lt;/a&gt;, it covers piano, electric guitar, and drums, it's free, and the &lt;a href="https://github.com/astraedus/piano" rel="noopener noreferrer"&gt;code is open&lt;/a&gt;. No account needed to use any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap I kept hitting
&lt;/h2&gt;

&lt;p&gt;Apps that gamify practice, streaks, XP, song-matching scores, are optimizing for a different thing than learning. They want you to open the app again tomorrow. That's a real goal and I get why it's the business model, but it produces a curriculum that's basically a leveled list: do lesson 4, then 5, then 6, with no visible reason 5 needed 4 first, and no way to tell if you're already past it.&lt;/p&gt;

&lt;p&gt;What I wanted instead was something that always knew exactly what I should practice next, and could tell me why, in one sentence, before I drilled it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real DAG, not a leveled list
&lt;/h2&gt;

&lt;p&gt;The curriculum is a directed acyclic graph. Every skill node carries its own prerequisites as explicit edges:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SkillNode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&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="c1"&gt;// "g-t1-power", "p-key-C-scale"&lt;/span&gt;
  &lt;span class="nl"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Instrument&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shared&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&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="nl"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SkillCategory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;prereqs&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="c1"&gt;// node ids, the DAG edges&lt;/span&gt;
  &lt;span class="nl"&gt;masteryDrill&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="nl"&gt;unlock&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="c1"&gt;// the capability sentence&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A node's live status resolves from its prereqs plus your progress:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SkillNode&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProgressMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Map&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;SkillNodeStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Locked if a prereq isn't learned yet. Available once every prereq is. In-progress once you've logged reps on it. Learned once you've finished it. &lt;code&gt;nextToLearn()&lt;/code&gt; filters for available nodes and sorts by tier, that's the entire "what should I practice tonight" decision. No hand-coded switch statement guessing what unlocks when, just prereq edges getting walked.&lt;/p&gt;

&lt;p&gt;Right now the tree has 32 piano nodes, 36 guitar nodes, and 20 drum nodes. Drums is practice-pad only for now, no full kit.&lt;/p&gt;

&lt;p&gt;The instrument-agnostic part matters as much as the DAG. Piano, guitar, and drums share one practice engine through an &lt;code&gt;InstrumentModule&lt;/code&gt; interface, and each instrument lives in its own &lt;code&gt;lib/&amp;lt;name&amp;gt;/module.tsx&lt;/code&gt; that self-registers on import. The only two components that actually know which instrument you're holding are the fretboard/keyboard visual and the tab/staff notation. The skill tree resolver, the review scheduler, the session generator, none of it cares what instrument it's looking at. Adding drums meant a new &lt;code&gt;lib/drums/&lt;/code&gt; folder and one import line, not a rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the research instead of guessing
&lt;/h2&gt;

&lt;p&gt;Before building the technique-drill and review systems I wrote up a &lt;a href="https://github.com/astraedus/piano/blob/main/docs/research/motor-learning.md" rel="noopener noreferrer"&gt;research pass on motor learning&lt;/a&gt;, because I didn't want to just intuit what "good practice" looks like. A few of those findings actually made it into the app as testable code, not vibes.&lt;/p&gt;

&lt;p&gt;Slow practice builds an accurate motor pattern. Jumping straight to target tempo doesn't. So technique drills ladder tempo up in small steps and only advance after a clean run at the current speed, no slider you can just drag to "hard."&lt;/p&gt;

&lt;p&gt;A skill "learned" once and never touched again quietly rots, so &lt;code&gt;REVIEW_INTERVALS_DAYS = [1, 3, 7, 14]&lt;/code&gt; brings finished nodes back at expanding intervals instead of letting them vanish the moment you pass them.&lt;/p&gt;

&lt;p&gt;And the ear training is honestly gated. The app never quizzes you on a chord progression it hasn't actually taught yet. Your effective ear level is clamped to whatever the skill tree has covered, so an advanced learner who self-reports at onboarding gets trusted, but nobody gets tested on theory they've never seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Next.js 16 and React 19, TypeScript throughout. Tone.js handles audio. VexFlow renders notation. svguitar draws chord diagrams. The skill graph itself, the visual tree on the roadmap page, renders with @xyflow/react and gets laid out with dagre. Nothing exotic, just the right library for each surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage: local-first, cloud optional
&lt;/h2&gt;

&lt;p&gt;Everything lives in localStorage by default. No account, no login wall, no server round-trip before you can play a note. That felt like the right default for something you open at night, tired, wanting to just start playing.&lt;/p&gt;

&lt;p&gt;If you want your progress on a second device, there's an opt-in cloud sync that only activates once you sign in. It's additive, never required, and the local copy stays the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm still filling out gaps in the guitar and drum curricula, and there's a batch of pop-song repertoire work queued that needs a real song list before it can ship. But the core loop, tell you what to practice, explain why, ladder the tempo, bring it back later, is live now.&lt;/p&gt;

&lt;p&gt;Music Practice is free and open source, live at &lt;a href="https://music.raeduslabs.com" rel="noopener noreferrer"&gt;music.raeduslabs.com&lt;/a&gt;. If you use it, even for five minutes, I want to know what confused you or what you wished it did next. Repo's open, issues welcome: &lt;a href="https://github.com/astraedus/piano" rel="noopener noreferrer"&gt;github.com/astraedus/piano&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>music</category>
    </item>
    <item>
      <title>TypeScript vs JavaScript: Which Should You Use in 2026</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:22:33 +0000</pubDate>
      <link>https://dev.to/astraedus/typescript-vs-javascript-which-should-you-use-in-2026-579e</link>
      <guid>https://dev.to/astraedus/typescript-vs-javascript-which-should-you-use-in-2026-579e</guid>
      <description>&lt;p&gt;Use TypeScript for anything you plan to keep past the weekend. Reach for plain JavaScript only for throwaway scripts, quick experiments, and learning the fundamentals. That answer hasn't really changed in years.&lt;/p&gt;

&lt;p&gt;What changed in 2026 is that the two oldest reasons to avoid TypeScript both died. Builds got roughly ten times faster, and Node now runs &lt;code&gt;.ts&lt;/code&gt; files with no build step at all. So if you've been putting off the decision because TypeScript felt heavy, the tax you were avoiding is mostly gone.&lt;/p&gt;

&lt;p&gt;I ship TypeScript across every app I build (React Native, Next.js, small Node services), and I still drop into plain JavaScript a few times a week. Here's where each one actually earns its place now.&lt;/p&gt;

&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%2Fm6g080sc3qs90xcovx51.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%2Fm6g080sc3qs90xcovx51.png" alt="Two rows showing the old arguments against TypeScript on the left and the 2026 reality on the right: builds are slow becomes TypeScript 7 Go compiler with roughly 10x faster builds, and you need a build step becomes Node 24 runs node script dot ts natively" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  They are not two competing languages
&lt;/h2&gt;

&lt;p&gt;TypeScript is not a separate runtime language. It's JavaScript with a type layer that gets erased before your code runs. Every TypeScript project ships as JavaScript underneath.&lt;/p&gt;

&lt;p&gt;So "TypeScript vs JavaScript" is really one question: do you want a compiler checking your types while you write, or not? Nothing you write in TypeScript exists at runtime except the JavaScript it compiles down to. That framing explains why every argument below is about writing code, not running it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two arguments against TypeScript died in 2026
&lt;/h2&gt;

&lt;p&gt;The classic case against TypeScript was always "the build is slow" and "I need a whole toolchain just to run one file." Both of those are gone as of mid-2026.&lt;/p&gt;

&lt;p&gt;First, the compiler got fast. &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/" rel="noopener noreferrer"&gt;TypeScript 7.0&lt;/a&gt; shipped on July 8, 2026 as a native port of the compiler written in Go (the project was known as Corsa, or &lt;code&gt;tsgo&lt;/code&gt;). Microsoft reports 8x to 12x faster full builds; independent benchmarks on type-heavy codebases land closer to 4x to 7x. Either way, the "tsc is slow" complaint that justified skipping types on large projects no longer holds. When you &lt;code&gt;npm install typescript&lt;/code&gt;, the Go-native compiler is what you get.&lt;/p&gt;

&lt;p&gt;Second, you no longer need a build step to run TypeScript locally. Node strips types natively now, and it's stable and unflagged in the current Node 24 LTS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# No tsc, no ts-node, no bundler. This just runs.&lt;/span&gt;
node script.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deno and Bun have run &lt;code&gt;.ts&lt;/code&gt; files directly for years, and Deno even type-checks as part of the run. Node caught up and made it the default.&lt;/p&gt;

&lt;p&gt;One caveat matters here. Node only strips "erasable" syntax. Enums, namespaces with runtime values, parameter properties (&lt;code&gt;constructor(private x: string)&lt;/code&gt;), and decorators are not plain types, so Node refuses to run them. More on that gotcha below.&lt;/p&gt;

&lt;h2&gt;
  
  
  When plain JavaScript still wins
&lt;/h2&gt;

&lt;p&gt;Plain JavaScript is the right tool when the code won't live long enough to earn a type system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A throwaway script or a one-off piece of automation.&lt;/li&gt;
&lt;li&gt;A tiny prototype you fully expect to delete.&lt;/li&gt;
&lt;li&gt;Learning the language, where types add ceremony before the concepts land.&lt;/li&gt;
&lt;li&gt;A no-build &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; snippet dropped straight into a browser page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those, a type checker is friction with no payoff. You're optimizing for speed of thought, not for the person who maintains this in six months, because there is no such person.&lt;/p&gt;

&lt;p&gt;There's also a middle ground people forget. You can get real type checking in a plain &lt;code&gt;.js&lt;/code&gt; file with JSDoc and a single comment, no &lt;code&gt;.ts&lt;/code&gt; extension and no build step:&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="c1"&gt;// @ts-check&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * @param {number} price
 * @param {number} taxRate
 * @returns {number}
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withTax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;taxRate&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;price&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;taxRate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;withTax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Editor flags this: string is not assignable to number&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a fully supported TypeScript feature, not a hack. Svelte's core was famously typed this way for years, with JSDoc instead of &lt;code&gt;.ts&lt;/code&gt; files.&lt;/p&gt;

&lt;h2&gt;
  
  
  When TypeScript wins (which is most of the time)
&lt;/h2&gt;

&lt;p&gt;If more than one person, or more than one week, will touch the code, TypeScript wins.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refactors stop being scary, because the compiler finds every call site you broke.&lt;/li&gt;
&lt;li&gt;Public APIs and libraries document themselves through their types.&lt;/li&gt;
&lt;li&gt;Teams stop guessing what shape an object is, because the answer is in the editor.&lt;/li&gt;
&lt;li&gt;AI-assisted coding gets a safety net. A 2025 study found that 94% of compile errors in AI-generated code were type-check failures, which is exactly the class of mistake types catch instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ecosystem has already voted. GitHub's &lt;a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/" rel="noopener noreferrer"&gt;Octoverse 2025 report&lt;/a&gt; put TypeScript at the top: in August 2025 it became the most-used language on GitHub by monthly contributors, passing Python. New projects rarely make the choice by hand anymore, because Next.js, Astro, SvelteKit, and Angular all scaffold TypeScript by default. For most new work, TypeScript is simply what you get unless you opt out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 decision, in one view
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing on one page.&lt;/p&gt;

&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%2Fliq3vyyyru87c8737dm1.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%2Fliq3vyyyru87c8737dm1.png" alt="Decision matrix comparing when to use plain JavaScript (throwaway scripts, tiny prototypes, learning, no-build browser snippets, with a JSDoc middle ground) versus when to use TypeScript (code that lives past a weekend, multiple people, future refactors, public APIs, AI-written code), footer reads default to TypeScript and the exceptions are small and short-lived" width="799" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two gotchas before you jump
&lt;/h2&gt;

&lt;p&gt;Two 2026 traps before you upgrade.&lt;/p&gt;

&lt;p&gt;The first is TypeScript 7 tooling lag. The Go-native compiler does not expose a stable programmatic API yet (that's targeted for 7.1). Tools that import the compiler and call into it are not guaranteed to work against 7.0 on day one. That includes typescript-eslint and the template checkers behind Vue, Svelte, and Astro. If your stack depends on those, pin the previous TypeScript 6 line for that tooling until the API stabilizes.&lt;/p&gt;

&lt;p&gt;The second is Node's erasable-syntax limit. Because &lt;code&gt;node script.ts&lt;/code&gt; only strips types, any TypeScript feature that emits runtime code will throw. Turn on a compiler flag so you catch it while type-checking instead of at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"erasableSyntaxOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that set, &lt;code&gt;tsc&lt;/code&gt; warns you the moment you reach for an &lt;code&gt;enum&lt;/code&gt; or a parameter property that Node cannot run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Default to TypeScript in 2026, and keep plain JavaScript for genuinely short-lived code. The two changes this year (a ~10x faster compiler and native &lt;code&gt;.ts&lt;/code&gt; execution) removed the friction that made "just use JavaScript" a reasonable call for real projects.&lt;/p&gt;

&lt;p&gt;Can't add a build step? You're still not stuck with untyped code. Add &lt;code&gt;// @ts-check&lt;/code&gt; and a few JSDoc comments and you keep most of the safety net. Otherwise, start it typed. The friction is what people were really voting against, not the types.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox → &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The 5 Things That Silently Break AI Agents in Production</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Mon, 27 Jul 2026 10:17:51 +0000</pubDate>
      <link>https://dev.to/astraedus/the-5-things-that-silently-break-ai-agents-in-production-3led</link>
      <guid>https://dev.to/astraedus/the-5-things-that-silently-break-ai-agents-in-production-3led</guid>
      <description>&lt;p&gt;Your AI agent works in the demo because the demo runs once, on a clean input, while you watch. Production is thousands of runs, on messy inputs, with nobody watching. The gap is almost never the model's reasoning. It's five plumbing failures that stay invisible until they cost you a bad night.&lt;/p&gt;

&lt;p&gt;I run a fleet of agents unattended, every day. The first time I left one going overnight, it reported a clean run. Every step logged "success." The actual work was half-finished, and it had burned a small fortune getting there. None of the five failures below throw an error. They quietly produce a wrong result and hand it to the next step, which trusts it completely.&lt;/p&gt;

&lt;p&gt;Here is the map. The same loop that shines in a demo has five silent failure points once it runs for real.&lt;/p&gt;

&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%2F8w4yykakt1o0lv1oe0sn.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%2F8w4yykakt1o0lv1oe0sn.png" alt="Where AI agents break: the plan, act, observe, remember loop with five failure points marked" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The agent forgets everything between steps
&lt;/h2&gt;

&lt;p&gt;The agent's plan lives only in the model's context window, and that window gets summarized, truncated, or reset on any long run. When it does, the plan goes with it. Then the agent redoes work it already finished, or flatly contradicts itself. The demo never hits this because the demo is short.&lt;/p&gt;

&lt;p&gt;Treat the context window as a cache, not your source of truth. The durable state lives in a store, and every step loads it back before it does anything.&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="c1"&gt;// BAD: the plan is stuffed into a prompt string. One summarization and it's gone.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Goal: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Done so far: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;doneList&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Next?`&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;next&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD: state lives in a store. Every step LOADS it, then saves.&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;goal&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="nl"&gt;done&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="p"&gt;};&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;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&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;loadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// re-hydrate from the store, every step&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&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="c1"&gt;// the model sees ALL prior state&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&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;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&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;decision&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 helpers here (&lt;code&gt;loadState&lt;/code&gt;, &lt;code&gt;saveState&lt;/code&gt;, and their friends further down) are yours to implement. The pattern is the point. If your agent has to remember something across steps, that memory belongs on disk or in a row, not in the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. It trusts its own "done"
&lt;/h2&gt;

&lt;p&gt;The agent believes its own report. The model says "I wrote the file," your code marks the task complete, and nobody checked the disk. But &lt;code&gt;success&lt;/code&gt; usually means the run finished, not that the artifact exists. When the model authored that field itself, it means even less.&lt;/p&gt;

&lt;p&gt;So check the world, not the claim. And check it well, because a stale artifact from a previous run will pass a lazy check.&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="c1"&gt;// BAD: trust the model's word.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write report.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;markComplete&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="c1"&gt;// result.success means "the run ended", not "the file is there"&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD: clear any stale copy, then prove the new one exists.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report.pdf&lt;/span&gt;&lt;span class="dl"&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="o"&gt;=&amp;gt;&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write report.pdf&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;stat&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report.pdf&lt;/span&gt;&lt;span class="dl"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;stat&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent reported done, but report.pdf isn't there&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one rule, "a claim is not a fact," kills a whole class of confabulation bugs. The agent can be wrong without meaning to, so your code shouldn't take it at its word.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A tool call "succeeds" but did nothing
&lt;/h2&gt;

&lt;p&gt;A tool can return without an error and still do the wrong thing, or nothing at all. An HTTP 200 can mean "accepted but queued," "partial write," or "no-op." An exit code of 0 means the program said it succeeded: &lt;code&gt;curl&lt;/code&gt; without &lt;code&gt;-f&lt;/code&gt; exits 0 on a 404, and &lt;code&gt;grep&lt;/code&gt; exits 1 when it simply found nothing. And &lt;code&gt;fetch&lt;/code&gt; doesn't reject on a 500 or a 403 either, only on a network failure, so "no exception" is a weaker signal than it looks.&lt;/p&gt;

&lt;p&gt;Validate the effect you wanted, not the status you got back. Read the write back out and confirm it landed.&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="c1"&gt;// BAD: no exception, so it worked, right? (fetch resolves on 500 and 403 too.)&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD: check the status, then read the record back.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`POST failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&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="s2"&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;id&lt;/span&gt; &lt;span class="p"&gt;}&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;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saved&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="s2"&gt;`&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="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;r&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="kc"&gt;null&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;saved&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;written&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write returned 200 but did not persist&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On an eventually-consistent store, poll with a short backoff instead of reading once, or a write that really landed can still 404 on the immediate read-back. I once watched an agent post the same update fifteen times: each call returned 200, a downstream queue silently dropped it, so the agent re-checked, saw nothing, and posted again.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Small errors compound into a broken chain
&lt;/h2&gt;

&lt;p&gt;The fourth failure is pure math. Assume steps fail independently, which is optimistic, and a step that works 95% of the time still poisons the chain. &lt;code&gt;0.95 ** 10&lt;/code&gt; is &lt;code&gt;0.60&lt;/code&gt;. Four in ten full runs now fail somewhere, and a three-step demo hid it completely.&lt;/p&gt;

&lt;p&gt;Retries buy most of it back, so build the chain to absorb flakiness: retry, dedupe, checkpoint.&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;reliability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0.5987... not "reliable" across ten steps&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;safeStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&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;key&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="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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alreadyDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// dedupe: skip work already recorded&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&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;withRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// absorb transient flakiness&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;recordResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                       &lt;span class="c1"&gt;// checkpoint so a failure resumes here&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&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;Three attempts at 95% is 99.99% per step, which is 99.88% across the ten. But that dedupe lives at the orchestrator level only. The downstream operation still has to be idempotent on its own: pass an idempotency key to the payment or API provider, because a crash between the call and the checkpoint will run it twice, and twice means double-charge, double-send, double-write.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The loop never stops (and neither does the bill)
&lt;/h2&gt;

&lt;p&gt;The fifth failure is the runaway loop. &lt;code&gt;while (!done) { await agent.step() }&lt;/code&gt; is one bad plan away from looping forever, and every iteration is a paid API call. Nobody notices in the demo because the demo finishes. Unattended, it can spin for hours and hand you an invoice instead of a result.&lt;/p&gt;

&lt;p&gt;Put hard ceilings on it that fail closed. When a limit trips, the agent stops and tells you, rather than grinding on.&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;class&lt;/span&gt; &lt;span class="nc"&gt;BudgetError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// A step cap AND a token/cost cap. Both fail closed.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&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="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BudgetError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;step budget exceeded&lt;/span&gt;&lt;span class="dl"&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;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BudgetError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token budget exceeded&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;out&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total_tokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// field name is provider-specific (see below)&lt;/span&gt;
  &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&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;Watch that &lt;code&gt;usage&lt;/code&gt; field. OpenAI gives you &lt;code&gt;total_tokens&lt;/code&gt;; Anthropic gives you &lt;code&gt;input_tokens&lt;/code&gt; and &lt;code&gt;output_tokens&lt;/code&gt; and no total. Read the wrong one and &lt;code&gt;tokens&lt;/code&gt; becomes &lt;code&gt;NaN&lt;/code&gt;, every &lt;code&gt;NaN &amp;gt; 200_000&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, and your cap silently never fires. That is this whole article in one line: no error, wrong result. And notice the throw is a &lt;code&gt;BudgetError&lt;/code&gt;, not a plain one. If a budget throw gets swallowed by the retry wrapper from failure #4, you re-burn the whole budget three times. That is the exact bill this guard exists to prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern under all five
&lt;/h2&gt;

&lt;p&gt;One idea ties these together: the model proposes, deterministic code disposes. Treat every model output as untrusted input to an ordinary program. Verify it against the real world. Put hard limits around the loop. Keep the LLM for the fuzzy judgment it's good at, and keep the guarantees in plain code that can't hallucinate.&lt;/p&gt;

&lt;p&gt;Here is the ship checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] State lives in a store, loaded back every step, not in the context window&lt;/li&gt;
&lt;li&gt;[ ] Every "done" is verified against the source of truth&lt;/li&gt;
&lt;li&gt;[ ] Tool calls are checked by effect, not by status code&lt;/li&gt;
&lt;li&gt;[ ] Steps are deduped and retried; the downstream op is idempotent&lt;/li&gt;
&lt;li&gt;[ ] The loop has a step cap and a real token/cost cap (mind the field name)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That list is the whole difference between a thing that demos well and a thing you can actually walk away from.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox → &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Give Your AI Agent Tool Use That Actually Works</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Fri, 24 Jul 2026 10:13:12 +0000</pubDate>
      <link>https://dev.to/astraedus/how-to-give-your-ai-agent-tool-use-that-actually-works-46a1</link>
      <guid>https://dev.to/astraedus/how-to-give-your-ai-agent-tool-use-that-actually-works-46a1</guid>
      <description>&lt;p&gt;Your AI agent works in the demo and breaks in production for one reason, and it's almost never the model's reasoning. It's the tool calls. The model picks the right tool, then hands it a hallucinated argument, or it trusts a tool result that quietly failed. Reliable tool use isn't a smarter model. It's a discipline: treat every tool call the model makes as untrusted input. Validate it against a schema before you run it, then verify the result before the agent believes it.&lt;/p&gt;

&lt;p&gt;I run an agent whose entire job is tool calls. It publishes posts, sends email, pushes git, drives a browser, mostly with no human watching. A plausible-but-wrong call there isn't a shrug in a chat window. It's a real email to the wrong person. So this problem is my whole day, and the fix below is what turned a flaky demo into something I can leave running.&lt;/p&gt;

&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%2F9rnadco95gfeza0eycez.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%2F9rnadco95gfeza0eycez.png" alt="The tool-use reliability loop: validate arguments, guard execution, verify the result, and feed structured errors back to the model to retry." width="800" height="883"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure nobody catches in the demo
&lt;/h2&gt;

&lt;p&gt;The classic tool-use bug is the model filling in a blank it should have refused to fill. Take a request like "schedule a meeting with Sarah next week." The model doesn't know which Sarah, which timezone, or which 30-minute slot, so it guesses. In a chatbot that guess is invisible. Wire the same model to a &lt;code&gt;send_calendar_invite&lt;/code&gt; tool and the guess becomes a real invite to the wrong Sarah at 3am her time.&lt;/p&gt;

&lt;p&gt;The model isn't being dumb. It picked the right tool. The system around the tool just believed a value it never should have trusted. That's the gap, and you close it with four cheap checkpoints between "the model wants to call a tool" and "the agent acts on the result."&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Design tools the model can't misuse
&lt;/h2&gt;

&lt;p&gt;Narrow, single-job tools beat broad API wrappers on reliability. A generic tool hands the model too much rope; a narrow one leaves almost nothing to hallucinate into.&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="c1"&gt;// Broad and dangerous: the model writes the SQL, you run whatever it invents.&lt;/span&gt;
&lt;span class="nf"&gt;queryDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&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;Row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="c1"&gt;// Narrow and safe: one job, typed inputs, nothing to guess into.&lt;/span&gt;
&lt;span class="nf"&gt;getInvoiceById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoiceId&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;Invoice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;queryDatabase&lt;/code&gt; lets a wrong guess drop a table. &lt;code&gt;getInvoiceById&lt;/code&gt; can only ever fetch one invoice by one id. Every tool you expose is attack surface for a confident mistake, so keep each one boring and specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Validate arguments before you execute
&lt;/h2&gt;

&lt;p&gt;Provider structured outputs (OpenAI's JSON schema mode, Anthropic tool use) guarantee the &lt;strong&gt;shape&lt;/strong&gt; of the arguments, not the &lt;strong&gt;correctness&lt;/strong&gt; of the values. Well-formed JSON can still say "invite bob@typo" or a duration of 900 minutes. So validate the arguments yourself, and treat them as untrusted input even when they parse.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&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 schema is the contract. The model's arguments are untrusted until they pass it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SendInvite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;attendeeEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;startsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;       &lt;span class="c1"&gt;// ISO 8601. No "next week" left to guess.&lt;/span&gt;
  &lt;span class="na"&gt;durationMins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;240&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;validateArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawArgs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SendInvite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawArgs&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&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="nx"&gt;issues&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;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Return the reason. Do NOT throw. The model reads this and fixes the call.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reason&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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&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;The same gate in Python is a Pydantic model. Validate inside a try/except and return the error string instead of raising, so the model gets the reason back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conint&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendInvite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;attendee_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;
    &lt;span class="n"&gt;starts_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                       &lt;span class="c1"&gt;# ISO 8601, validated downstream
&lt;/span&gt;    &lt;span class="n"&gt;duration_mins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;conint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SendInvite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_args&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important move is the return, not the check. A thrown exception kills the run. A structured &lt;code&gt;{ ok: false, error }&lt;/code&gt; gets handed back to the model, which retries with the reason (see the retry lane in the diagram above).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Guard the execution itself
&lt;/h2&gt;

&lt;p&gt;A valid tool call can still cause damage if it runs twice or can't be undone. So make writes idempotent, and put a confirmation gate in front of anything irreversible.&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;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&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;guardedExecute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Same key already ran? Return the prior result instead of doing it twice.&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;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;already done, skipped duplicate&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="c1"&gt;// Irreversible actions (send, charge, delete) need an explicit gate.&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;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;irreversible&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isConfirmed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;needs confirmation before running&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;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&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 &lt;code&gt;Set&lt;/code&gt; here is illustrative. In production the key lives in Redis or your database, so a restart or a second worker still sees it. Agents retry constantly (networks time out and the model tries again), and without a durable idempotency key you send the invite twice. Give each write a stable key, gate the irreversible actions, and scope every tool's credentials to just its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Verify the result, don't trust it
&lt;/h2&gt;

&lt;p&gt;A tool returning &lt;code&gt;success: true&lt;/code&gt; is a claim, not proof. Verify the outcome against the source of truth before the agent acts on it, because tool-call failures compound: one wrong result quietly poisons every step after 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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callWithVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;propose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxTries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastError&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxTries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&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;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;propose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// model re-proposes given the last error&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;guardedExecute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&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="o"&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lastError&lt;/span&gt; &lt;span class="o"&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;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The tool said "done". Prove it against the source of truth.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verified&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;sourceOfTruthHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&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;verified&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;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;lastError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool reported success but the change was not found&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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`gave up after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maxTries&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tries: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;For the invite, &lt;code&gt;sourceOfTruthHas&lt;/code&gt; reads the calendar API back and checks the event exists. For a git push, it queries the remote. The rule is the same one good engineers already live by: never derive a fact from a self-report when you can read the source. A capped retry loop keeps a confused model from burning your token budget forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Reliable tool use comes from four cheap habits, not a bigger model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Narrow tools&lt;/strong&gt; so there's little to hallucinate into.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validated arguments&lt;/strong&gt;, treated as untrusted input, with the failure reason handed back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarded execution&lt;/strong&gt;: idempotent writes, a gate on anything irreversible, least-privilege credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verified results&lt;/strong&gt; checked against the source of truth, not the tool's own "success."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model proposes; your code disposes. Wrap those four checkpoints around every tool and the same model that flaked in your demo becomes an agent you can actually leave running.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox → &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
      <category>python</category>
    </item>
    <item>
      <title>The TypeScript Gotcha That Silently Breaks Production: Types Vanish at Runtime (And How to Fix It)</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Wed, 22 Jul 2026 10:16:15 +0000</pubDate>
      <link>https://dev.to/astraedus/the-typescript-gotcha-that-silently-breaks-production-types-vanish-at-runtime-and-how-to-fix-it-12n9</link>
      <guid>https://dev.to/astraedus/the-typescript-gotcha-that-silently-breaks-production-types-vanish-at-runtime-and-how-to-fix-it-12n9</guid>
      <description>&lt;p&gt;TypeScript's types don't exist at runtime. The compiler deletes every interface and type annotation before your code runs, so &lt;code&gt;const user = data as User&lt;/code&gt; is a promise the compiler can't keep. The day the real data doesn't match, production throws an error the type checker swore was impossible.&lt;/p&gt;

&lt;p&gt;You trust the green check. &lt;code&gt;tsc&lt;/code&gt; passes. Your tests pass. You ship on Friday. Then Monday a &lt;code&gt;Cannot read properties of undefined (reading 'name')&lt;/code&gt; lands in your error tracker, on a line the type system called safe. Nothing in your code changed. An API you consume changed one field, and TypeScript never noticed.&lt;/p&gt;

&lt;p&gt;Here is the whole article in one picture:&lt;/p&gt;

&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%2Fveemrtpjerpt1xxk4q56.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%2Fveemrtpjerpt1xxk4q56.png" alt="Compile-time types are erased; at runtime an  raw `as` endraw  cast lies while a schema validates" width="799" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I ship apps that consume data from other people's APIs. Here is the shape of a bug I have watched happen more than once. One response field's type said &lt;code&gt;number&lt;/code&gt;. Then a vendor shipped an update and started returning it as a string, sometimes &lt;code&gt;null&lt;/code&gt;. My code did the obvious thing: &lt;code&gt;const profile = await res.json() as Profile&lt;/code&gt;. Every type looked right in the editor. The build passed. And a screen that read &lt;code&gt;profile.score.toFixed(1)&lt;/code&gt; started crashing for a slice of users, because &lt;code&gt;score&lt;/code&gt; now arrived as a string and strings have no &lt;code&gt;toFixed&lt;/code&gt;. No compiler error. No test failure. Just a runtime explosion at the exact spot TypeScript promised was safe.&lt;/p&gt;

&lt;p&gt;That bug isn't special. It's the single most common way TypeScript lets you down, and it has one root cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your types disappear
&lt;/h2&gt;

&lt;p&gt;TypeScript is a compile-time tool. When you run &lt;code&gt;tsc&lt;/code&gt;, it checks your types and then throws them away. Transpilers like esbuild, swc, and Babel skip the check entirely; they just strip the types out. Interfaces, type aliases, and annotations emit zero runtime code. What ships to production is plain JavaScript with no memory that types ever existed.&lt;/p&gt;

&lt;p&gt;Watch what happens to a simple file:&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="c1"&gt;// user.ts  (what you wrote)&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;name&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&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="s2"&gt;`Hi &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// user.js  (what actually runs)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="s2"&gt;`Hi &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;The &lt;code&gt;interface User&lt;/code&gt; is gone. Not minified, not hidden. Gone. At runtime there is no &lt;code&gt;User&lt;/code&gt;, no &lt;code&gt;id: number&lt;/code&gt; check, nothing that inspects the shape of &lt;code&gt;user&lt;/code&gt;. (Enums and classes are the exception; they emit real JavaScript. Interfaces and type aliases never do.)&lt;/p&gt;

&lt;p&gt;So a type annotation is a note to the compiler, not a guard on your data. That distinction is where production breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lie hides at the boundary
&lt;/h2&gt;

&lt;p&gt;Inside code you fully control, TypeScript is excellent. It tracks types across functions and catches real mistakes. The danger starts where data crosses into your program from a place the compiler cannot see.&lt;/p&gt;

&lt;p&gt;Every one of these hands you &lt;code&gt;any&lt;/code&gt; or an unchecked value:&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;data&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;res&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="c1"&gt;// any&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&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;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;// any&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// string | null&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&lt;/span&gt;            &lt;span class="c1"&gt;// string | undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;any&lt;/code&gt; is the real problem. It's the one type that turns the checker off. Assign &lt;code&gt;any&lt;/code&gt; to a typed variable and TypeScript stops complaining, because &lt;code&gt;any&lt;/code&gt; is assignable to everything:&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;Profile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;score&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Profile&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;res&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="c1"&gt;// no error, ever&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;      &lt;span class="c1"&gt;// crashes if score is a string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cast version is worse, because it looks deliberate and careful:&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;profile&lt;/span&gt; &lt;span class="o"&gt;=&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;res&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;Profile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;as&lt;/code&gt; isn't a conversion. It doesn't check anything. It's you telling the compiler "trust me, this is a &lt;code&gt;Profile&lt;/code&gt;," and the compiler obeying without looking. If you're wrong, nobody finds out until a user hits the broken screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: parse, don't cast
&lt;/h2&gt;

&lt;p&gt;Don't tell the compiler what the data is. Prove it at runtime, at the moment it enters. Use a schema validator (Zod, Valibot, and ArkType all do this). Derive your static type from that schema, so you have one source of truth instead of two that drift apart.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&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;Profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;   &lt;span class="c1"&gt;// the static type, generated from the schema&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Profile&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// profile is now a real Profile, checked field by field&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="c1"&gt;// safe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the API returns &lt;code&gt;score&lt;/code&gt; as a string, &lt;code&gt;.parse()&lt;/code&gt; throws immediately, at the boundary, with a precise message naming the field that was wrong. The bug surfaces where it enters, not three screens later in code that looks innocent.&lt;/p&gt;

&lt;p&gt;When you don't want to throw, use &lt;code&gt;safeParse&lt;/code&gt; and handle the failure like any other bad input:&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&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;res&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;logBadResponse&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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// you decide: retry, default, alert&lt;/span&gt;
  &lt;span class="k"&gt;return&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;profile&lt;/span&gt; &lt;span class="o"&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;data&lt;/span&gt;      &lt;span class="c1"&gt;// typed and trusted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is three words: parse, don't cast. A cast asserts. A parse verifies. Only one of them survives contact with real data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harden the rest of your boundaries
&lt;/h2&gt;

&lt;p&gt;The API response is the obvious hole. The same class of bug hides in quieter places.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; returns whatever a past release wrote, which may not match this release's shape:&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Settings is a Zod schema you defined, same idea as Profile above&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Settings&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;JSON&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;raw&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{}&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;Environment variables are strings or &lt;code&gt;undefined&lt;/code&gt;, never the numbers and booleans you treat them as. Validate them once, at startup, so a missing key fails on boot instead of at 3am:&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;Env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Env&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// crashes on boot if misconfigured&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two tsconfig flags close the remaining gaps. &lt;code&gt;strict&lt;/code&gt; turns on the checks that matter. &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; stops TypeScript from lying about array and record access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUncheckedIndexedAccess"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that second flag, &lt;code&gt;arr[10]&lt;/code&gt; is typed as your element type even when the array holds three items. With it, the type becomes &lt;code&gt;T | undefined&lt;/code&gt; and the compiler makes you handle the empty case. One line of config kills a whole family of "undefined is not a function" crashes.&lt;/p&gt;

&lt;p&gt;One last habit: prefer &lt;code&gt;unknown&lt;/code&gt; over &lt;code&gt;any&lt;/code&gt; at every edge. &lt;code&gt;unknown&lt;/code&gt; also accepts anything, but it refuses to let you touch the value until you narrow it, so the compiler pushes you toward validating instead of assuming.&lt;/p&gt;

&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%2F6vpn6xui9npacwfozhy1.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%2F6vpn6xui9npacwfozhy1.png" alt="Boundary checklist: validate res.json, JSON.parse, localStorage, env, and input; enable strict + noUncheckedIndexedAccess; prefer unknown over any" width="800" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;TypeScript guarantees consistency inside the boundary it can see. It guarantees nothing about data crossing into that boundary from an API, a file, storage, or a user. Your types are a compile-time contract, and the compiler tears the contract up before your code runs.&lt;/p&gt;

&lt;p&gt;So draw the line yourself. Validate at every edge with a schema, generate your types from that schema, and trust your types everywhere inside. Parse, don't cast. Do that, and the green check finally means what you always assumed it meant.&lt;/p&gt;

&lt;p&gt;What's the worst runtime type bug that walked straight past &lt;code&gt;tsc&lt;/code&gt; for you? Mine is the one up top: a vendor quietly flipping a &lt;code&gt;number&lt;/code&gt; field to a string. Tell me yours in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox → &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Next.js in 2026: The 5 Features That Change How You Ship (And 2 Still Missing)</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Mon, 20 Jul 2026 10:13:10 +0000</pubDate>
      <link>https://dev.to/astraedus/nextjs-in-2026-the-5-features-that-change-how-you-ship-and-2-still-missing-415j</link>
      <guid>https://dev.to/astraedus/nextjs-in-2026-the-5-features-that-change-how-you-ship-and-2-still-missing-415j</guid>
      <description>&lt;p&gt;Next.js 16 didn't add a pile of new APIs. It changed the defaults you never think about: how your app builds, whether a page is cached, and which file intercepts a request. If you're upgrading from 15, five of those changes show up in your day, and two things I wanted still aren't here.&lt;/p&gt;

&lt;p&gt;I run Next.js 16 in production on a small app (16.2 with Neon and Clerk), so this is the upgrade as it actually lands, not the changelog.&lt;/p&gt;

&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%2Fx18ud66q6kwngx0vvbcw.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%2Fx18ud66q6kwngx0vvbcw.png" alt="Where each Next.js 16 feature lives in a request: build with Turbopack, request through proxy.ts, render dynamic-by-default with use cache, React Compiler and React 19.2, then mutate with updateTag, refresh, and revalidateTag" width="800" height="856"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Turbopack is the default bundler
&lt;/h2&gt;

&lt;p&gt;Turbopack is stable now and the default for every new project. You get 2 to 5x faster production builds and up to 10x faster Fast Refresh, with no config change. That's the single most noticeable difference on day one. On my own app (16.2), a clean production build lands in about 19 seconds, 8.3 of them Turbopack compiling, and I never touched the bundler config.&lt;/p&gt;

&lt;p&gt;If you've got a custom webpack setup that Turbopack can't handle yet, opt out per command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;next dev &lt;span class="nt"&gt;--webpack&lt;/span&gt;
next build &lt;span class="nt"&gt;--webpack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turbopack also caches compiler artifacts to disk between runs. For &lt;code&gt;next dev&lt;/code&gt; that filesystem cache is on by default since 16.1, so your second &lt;code&gt;dev&lt;/code&gt; start is faster for free. The same cache for &lt;code&gt;next build&lt;/code&gt; is still experimental, and you opt in (more on that below):&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="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;turbopackFileSystemCacheForBuild&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// experimental&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Caching is opt-in now with "use cache"
&lt;/h2&gt;

&lt;p&gt;This is the biggest mental shift, and the one that breaks the most assumptions. In Next.js 16, every page, layout, and route handler runs at request time by default. Nothing's silently cached. You opt a page or component into the cache with the &lt;code&gt;"use cache"&lt;/code&gt; directive, and the compiler generates the cache key for you.&lt;/p&gt;

&lt;p&gt;Turn it on in config first:&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="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then mark what you actually want cached:&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="c1"&gt;// app/products/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;ProductsPage&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;use cache&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;products&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductGrid&lt;/span&gt; &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;products&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;The old implicit caching is gone. So are the &lt;code&gt;experimental.ppr&lt;/code&gt; and &lt;code&gt;experimental.dynamicIO&lt;/code&gt; flags, which the new Cache Components model replaces. If you upgrade a 15 app and your data suddenly looks fresher (and your database busier), that's why. Pages that used to be static now hit the database on every request, until you add &lt;code&gt;"use cache"&lt;/code&gt; back where you meant it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. middleware.ts is proxy.ts now
&lt;/h2&gt;

&lt;p&gt;This is the one that caught me on upgrade. &lt;code&gt;middleware.ts&lt;/code&gt; is deprecated in favor of &lt;code&gt;proxy.ts&lt;/code&gt;. The rename itself is mechanical, and a codemod does it for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@canary middleware-to-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It renames the file and the exported function:&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="c1"&gt;// proxy.ts (was middleware.ts)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part that isn't mechanical is the runtime. &lt;code&gt;proxy.ts&lt;/code&gt; runs on the Node.js runtime, and it's meant to stay a thin proxy that clarifies your network boundary. If your old middleware leaned on Edge-runtime behavior, that's the bit to check, not the rename. My Clerk auth check moved over fine once I renamed the file and the function, but I would not have found that out from the changelog. &lt;code&gt;middleware.ts&lt;/code&gt; still works for Edge cases, but it's deprecated and gets removed later.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. New cache invalidation: updateTag() and refresh()
&lt;/h2&gt;

&lt;p&gt;Once caching is explicit, invalidation has to be too. Next.js 16 splits it into three intents, and picking the wrong one is how you ship a stale UI. Here's the decision I make every time:&lt;/p&gt;

&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%2Fzphdq2uqphrd83t2g2lz.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%2Fzphdq2uqphrd83t2g2lz.png" alt="Cache invalidation decision guide: use updateTag for read-your-writes inside a Server Action, revalidateTag with a cacheLife profile for shared content with stale-while-revalidate, and refresh for uncached data shown elsewhere" width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;updateTag()&lt;/code&gt; is new and Server-Actions-only. It expires the cache and reads fresh data in the same request, so the user sees their own write immediately:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;saveProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// read-your-writes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;revalidateTag()&lt;/code&gt; now requires a cache profile as its second argument, which enables stale-while-revalidate. The single-argument form is deprecated:&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="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog-posts&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;max&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// serve cached now, revalidate in background&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;refresh()&lt;/code&gt; re-fetches uncached data shown elsewhere on the page (a notification count, a live metric) without touching the cache at all. Three intents, three functions, no guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The React Compiler is stable
&lt;/h2&gt;

&lt;p&gt;Built-in React Compiler support is stable now, following the compiler's 1.0 release. It automatically memoizes components and cuts unnecessary re-renders with zero code changes. It's not on by default, because it relies on Babel and can slow your build, so you enable it deliberately:&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="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;reactCompiler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also inherit React 19.2 in the App Router: View Transitions for animating navigations, &lt;code&gt;useEffectEvent&lt;/code&gt; for non-reactive Effect logic, and &lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt; for keeping background UI mounted with its state intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2 I'm still waiting for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Filesystem build caching that's stable, not experimental.&lt;/strong&gt; The dev filesystem cache is on by default now, and it's genuinely good. The build cache (&lt;code&gt;turbopackFileSystemCacheForBuild&lt;/code&gt;) is still experimental, and that's the one I want most. A cold CI runner rebuilding from scratch on every push is exactly where a persistent compiler cache pays off. It's also exactly where I don't want to lean on an experimental flag yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One caching mental model instead of four functions.&lt;/strong&gt; Cache Components is the right direction. But shipping it means holding &lt;code&gt;"use cache"&lt;/code&gt;, &lt;code&gt;updateTag()&lt;/code&gt;, &lt;code&gt;revalidateTag(tag, profile)&lt;/code&gt;, and &lt;code&gt;refresh()&lt;/code&gt; in your head at once, plus a &lt;code&gt;cacheLife&lt;/code&gt; profile system on top. It's powerful. It's also a lot of surface area for "cache this and invalidate it correctly." I want the diagram above to eventually be one function.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you upgrade one 15 app to 16 this week, budget for exactly two things. Rename &lt;code&gt;middleware.ts&lt;/code&gt; to &lt;code&gt;proxy.ts&lt;/code&gt; (and check the runtime, not just the filename), then go find every place that relied on implicit caching and add &lt;code&gt;"use cache"&lt;/code&gt; back. Turbopack, the React Compiler, and the new invalidation APIs are upside you mostly get for free. The dynamic-by-default flip is the one that'll surprise you in production if you skip it. Run the codemod, read your build logs, and cache on purpose.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox -&amp;gt; &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>React Native vs Flutter in 2026: When Each One Actually Wins</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Mon, 13 Jul 2026 10:19:17 +0000</pubDate>
      <link>https://dev.to/astraedus/react-native-vs-flutter-in-2026-when-each-one-actually-wins-ac4</link>
      <guid>https://dev.to/astraedus/react-native-vs-flutter-in-2026-when-each-one-actually-wins-ac4</guid>
      <description>&lt;p&gt;React Native and Flutter are both production-ready in 2026, so the honest answer to "which one should I use" isn't "the faster one." It comes down to two things: your team and your UI. If your people already write JavaScript and React, React Native wins. If your product is a custom-drawn interface that has to look identical on every screen, Flutter wins. I ship four apps on Google Play, all React Native, and even I'll tell you the speed benchmark is the wrong reason to pick either one.&lt;/p&gt;

&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%2Fh4t9hzmrvebffnb48gct.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%2Fh4t9hzmrvebffnb48gct.png" alt="React Native vs Flutter in 2026: a two-column card showing when to choose each framework" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole game, and most "X vs Y" posts miss it. I don't reach for React Native because it beats Flutter on a chart. I reach for it because my team already thinks in React. So let me draw the actual boundary, starting with the stale objections that both frameworks already killed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did the old criticisms even survive to 2026?
&lt;/h2&gt;

&lt;p&gt;Mostly no, and that's the first thing to fix. Half the comparison posts online still argue about problems both frameworks already solved.&lt;/p&gt;

&lt;p&gt;React Native's slow "bridge" is gone. The New Architecture (Fabric plus TurboModules, talking to native code directly through JSI) has been the default since React Native 0.76 and mandatory since 0.82. The old bridge has since been removed entirely. The current release is 0.86, shipped in June 2026. So don't call React Native "bridge-based" anymore. It stopped being true a while ago.&lt;/p&gt;

&lt;p&gt;Flutter's old shader jank is gone too. The Impeller renderer precompiles shaders at build time, which killed the first-run animation stutter. It's been the default on iOS for years and became the default on Android (API 29 and up) in the 3.27 release. The current stable is Flutter 3.44 on Dart 3.12. Both frameworks grew up. Compare the 2026 versions, not the 2021 ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does React Native actually win?
&lt;/h2&gt;

&lt;p&gt;React Native wins when your team and your codebase already speak JavaScript. That single fact decides more projects than any benchmark.&lt;/p&gt;

&lt;p&gt;Here's why it compounds. Your developers reuse the React mental model they already have: components, hooks, JSX. You share validation, types, and business logic with a React web app. You pull from npm, the largest package registry in software. And with Expo you ship JavaScript-only fixes over the air, skipping the app-store review queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# React Native + Expo: push a JS-only fix, no store review&lt;/span&gt;
eas update &lt;span class="nt"&gt;--branch&lt;/span&gt; production &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"hotfix: null guard on profile"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component itself is just React:&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="c1"&gt;// React Native: a counter, in JSX + hooks&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Pressable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&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;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&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;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="nc"&gt;Pressable&lt;/span&gt; &lt;span class="na"&gt;onPress&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;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&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="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="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Tapped &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; times&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&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="nc"&gt;Pressable&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;If that code looks familiar, React Native is your shortcut. You hire from the huge pool of web developers, and they're productive on day one. You'll also find React Native inside big production apps like Shopify and Microsoft.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does Flutter actually win?
&lt;/h2&gt;

&lt;p&gt;Flutter wins when the UI is the product. If you're drawing a custom design language and every pixel matters, Flutter was built for exactly that.&lt;/p&gt;

&lt;p&gt;Flutter doesn't use the platform's native widgets. It paints every pixel itself with Impeller, so a screen looks identical on an old Android phone and a new iPhone. That control shines in animation-heavy, brand-heavy interfaces aiming for 120fps. You also ship one compiled binary. Dart builds ahead of time to native machine code, with no JavaScript runtime living inside your app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Flutter: the same counter, in Dart widgets&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_CounterState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_CounterState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&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="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Tapped &lt;/span&gt;&lt;span class="si"&gt;$count&lt;/span&gt;&lt;span class="s"&gt; times'&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Flutter: one compiled binary, ahead-of-time, no JS runtime&lt;/span&gt;
flutter build appbundle &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape is different. Flutter has its own widget and state model, and you write it in Dart. If your team has no web background to reuse, that's not a cost. It's a clean slate. Flutter also reaches further out of the box: the same codebase runs on mobile, desktop, and embedded devices. Google Pay and Nubank are built this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which one is faster?
&lt;/h2&gt;

&lt;p&gt;For the app most teams actually build, you won't feel a difference in 2026. Lists, forms, navigation, and network calls run smoothly on both.&lt;/p&gt;

&lt;p&gt;Flutter keeps an edge when graphics get heavy. Compiled Dart plus Impeller handles dense animation and custom rendering with less effort. React Native's New Architecture closed the old gap for normal app work, because native calls are now synchronous and Hermes runs the JavaScript fast. So pick on developer experience, not on a micro-benchmark, unless you're building something game-like or visually intense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is either one dying in 2026?
&lt;/h2&gt;

&lt;p&gt;No, and this matters if you're betting a product on your choice. Both frameworks got fresh institutional backing in the last year.&lt;/p&gt;

&lt;p&gt;Meta moved React and React Native under a new React Foundation in late 2025, with a governing board that includes Amazon, Microsoft, and Vercel. Google kept shipping Flutter on its steady roughly-quarterly cadence, and handed desktop stewardship to Canonical so its own team can focus on mobile and AI tooling. Neither project is coasting. You're choosing between two well-funded, actively maintained frameworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real tie-breaker
&lt;/h2&gt;

&lt;p&gt;Two questions settle it. Does your team already write React and JavaScript? And is your UI a standard native feel or a fully custom design language?&lt;/p&gt;

&lt;p&gt;Cross those answers. A React team plus a standard UI points hard at React Native. No web background plus a bespoke, animated design points hard at Flutter. Everything else is a close call where either one ships a great app. So pick the framework your team can move fastest in. For most teams shipping a standard app, that's the stack they already know, and that alone is a legitimate reason to choose it. Both are safe bets in 2026. The only real mistake is choosing on a benchmark you'll never actually feel.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox → &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>flutter</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The AI Agents Gotcha That Breaks Production (And How to Fix It)</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Fri, 10 Jul 2026 10:11:44 +0000</pubDate>
      <link>https://dev.to/astraedus/the-ai-agents-gotcha-that-breaks-production-and-how-to-fix-it-5a15</link>
      <guid>https://dev.to/astraedus/the-ai-agents-gotcha-that-breaks-production-and-how-to-fix-it-5a15</guid>
      <description>&lt;p&gt;Here is the gotcha: an AI agent's process state is not the world state. An agent run ends one of two ways you can see: a success report, or a crash. Neither tells you whether its side effects actually happened. Production breaks in the gap: you retry a dead agent that already acted and fire the action twice, or you trust a success report for an action that never ran.&lt;/p&gt;

&lt;p&gt;I run autonomous agents in production every day. This one gotcha has bitten me from both sides in the same week. One killed worker nearly re-submitted the same listing to a directory that reviews duplicates by hand.&lt;/p&gt;

&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%2F0ju5qqbl20zht7mtdzov.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%2F0ju5qqbl20zht7mtdzov.png" alt="The four states of a finished agent run: the report says success or death, the world changed or did not, and the exit code cannot tell them apart" width="799" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is how the near-duplicate happened. A billing limit killed one of my browser automation workers mid-run. It died with 84 bytes of stdout. I read that as total failure and started redoing its jobs from the top. An hour later I found the truth in its leftover browser tabs. The worker had already completed one directory submission (the listing was live), and had a second one half-filled in an open form. My "redo everything" pass was minutes away from submitting the live listing a second time.&lt;/p&gt;

&lt;p&gt;That is the whole trap in one story. Now let's generalize it, because your stack has the same bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do retries double-fire side effects?
&lt;/h2&gt;

&lt;p&gt;A dead process is not undone work. When a worker dies, you lose the report. Every external action it completed stays completed.&lt;/p&gt;

&lt;p&gt;Most agent pipelines start with retry logic like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publish_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# If the first run already published before it died,
&lt;/span&gt;    &lt;span class="c1"&gt;# this line publishes it twice.
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publish_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is perfectly safe for a pure function. It is a bug for anything that does a POST, submits a form, sends an email, or moves money. The retry silently assumes the failed run changed nothing. Look at the quadrant diagram above: that assumption only holds in one of the two "agent died" states, and you cannot tell them apart from the exit code.&lt;/p&gt;

&lt;p&gt;LLM agents make this worse than ordinary jobs, for two reasons. Their tasks are long, so death lands mid-sequence with three of seven actions done. And their actions live on other people's platforms, where a duplicate submission, comment, or charge is visible, embarrassing, and sometimes unrecoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can't you trust an agent's success report?
&lt;/h2&gt;

&lt;p&gt;An agent's "done" is generated text, not a receipt.&lt;/p&gt;

&lt;p&gt;My publishing agent once reported a clean cross-post to a second platform. The API token was missing from its environment at runtime, so the publish had silently failed. The agent still summarized the task as complete. Our outcomes poller caught the lie a day later by querying the platform and finding nothing.&lt;/p&gt;

&lt;p&gt;Another time, a read-only monitoring agent reported that a human had clicked a confirmation button. It had no way to observe a click. It inferred the click from a misread screenshot and stated the inference as fact. That false "resolved" sat in a status file, hiding a deadline that mattered.&lt;/p&gt;

&lt;p&gt;Neither agent was broken. This is just what language models do: they produce the most plausible completion of "report on your task." A plausible report and a true report look identical from inside the transcript. The only way to tell them apart is to look outside the transcript.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you fix it?
&lt;/h2&gt;

&lt;p&gt;Treat every agent task like an unreliable network call with side effects. That gives you three concrete rules: verify against the source of truth, put an idempotency key on every external action, and reconcile before any retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: verify "done" against the platform, never against the report.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;USERNAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;astraedus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# your Dev.to username
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;actually_published&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api/articles/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;USERNAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publish_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;actually_published&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unverified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's report never touches your state store. Only the platform's answer does. When you cannot verify, record &lt;code&gt;unverified&lt;/code&gt;, not &lt;code&gt;done&lt;/code&gt;. An honest unknown beats a confident fiction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2: give every external action an idempotency key.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ledger below is any durable key-value store. A dict works for a demo; use SQLite or Redis in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shelve&lt;/span&gt;

&lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shelve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;side_effects.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;submit:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;app_slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# This action already fired once. Return the recorded
&lt;/span&gt;    &lt;span class="c1"&gt;# outcome instead of re-running the side effect.
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;submit_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Payment APIs like Stripe support idempotency keys exactly so a retried request cannot charge a card twice. Your agent pipeline needs the same discipline for the same reason. The key comes from what the action does, not from which run attempts it. A second run computes the same key, finds it in the ledger, and gets the stored outcome back. The side effect never fires twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3: when a run dies, reconcile before you retry.&lt;/strong&gt;&lt;/p&gt;

&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%2F1p3kozr7qyr2jy427hvp.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%2F1p3kozr7qyr2jy427hvp.png" alt="Reconcile-then-retry flow" width="800" height="903"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before re-dispatching anything, inventory what the dead worker left behind: files it wrote, the git status of any repo it touched, its open browser tabs or sessions, and the live URLs it may have created. Each artifact tells you which actions completed. The recovery loop is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reconcile_then_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;evidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# files, git status, live URLs, open sessions
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                          &lt;span class="c1"&gt;# fired and recorded: skip
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visible_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# fired, never recorded: record
&lt;/span&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# genuinely missing: safe to run
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my directory incident, a 30-second tab inventory would have shown me the live listing and saved the near-duplicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you do differently tomorrow?
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;side_effects&lt;/code&gt; table and make every retry path call reconcile first. The one detail that matters most: assign the idempotency key before the agent runs, not after it fails. A key created during recovery cannot protect the run that just died.&lt;/p&gt;

&lt;p&gt;None of this needs a framework. A hash function, a key-value table, and the platform's read API cover all three rules. What it needs is the mental shift: your agent does not tell you what happened. The world does.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these from real work at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt;, where I build apps and tools. Building something, or stuck on something like this? Reach me at &lt;a href="https://astraedus.dev" rel="noopener noreferrer"&gt;astraedus.dev&lt;/a&gt; or &lt;a href="mailto:theagentthatcould@gmail.com"&gt;theagentthatcould@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Get the next one in your inbox, &lt;a href="https://astraedus.dev/#subscribe" rel="noopener noreferrer"&gt;subscribe at astraedus.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Mood Trackers Without a Subscription (Free, No Paywall)</title>
      <dc:creator>Diven Rastdus</dc:creator>
      <pubDate>Thu, 09 Jul 2026 22:43:40 +0000</pubDate>
      <link>https://dev.to/astraedus/mood-trackers-without-a-subscription-free-no-paywall-3291</link>
      <guid>https://dev.to/astraedus/mood-trackers-without-a-subscription-free-no-paywall-3291</guid>
      <description>&lt;p&gt;There's a special kind of irony in paying a monthly fee to write down how you feel. You install a mood tracker to understand your own head, and three taps later there's a screen offering you "Premium" for a recurring charge, with your fuller stats and half the features waiting behind it. The thing you built to look after yourself now has a subscription attached, right next to your streaming, your storage, and everything else quietly draining the account.&lt;/p&gt;

&lt;p&gt;Mood tracking does not need to cost anything, and it definitely doesn't need to be recurring. A mood log is text and numbers in a small local database. The compute is trivial. So when an app charges monthly, you're not paying for the tracking, you're paying for a business model. This is a straight look at which popular trackers put a subscription in your way, what they actually charge, and the genuinely free, no-paywall options, including the app I built, the SoulSync mood tracker.&lt;/p&gt;

&lt;p&gt;Prices below were checked at the time of writing. App pricing changes and varies by region, so treat any number as "confirm it on the store," and where a current price couldn't be pinned down cleanly I've said so instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How mood tracking turned into a subscription
&lt;/h2&gt;

&lt;p&gt;Freemium works like this. The app is free to install and free to start, which gets it onto your phone and into your routine. Then the features that make it feel finished, the deeper stats, the extra moods, the correlations, the themes, sit behind a recurring payment. By the time you hit the wall, you've already got weeks of history in the app and you're the least likely to walk away. That's the design, and it's a good business. It's just not in your interest.&lt;/p&gt;

&lt;p&gt;The tell is that the paywalled features are almost never expensive to provide. Showing you a chart of your own data doesn't cost the company anything per month. The subscription isn't priced to the cost of serving you. It's priced to what a captured user will pay. Recognising that makes it much easier to walk past the upgrade screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who charges a subscription (and roughly what)
&lt;/h2&gt;

&lt;p&gt;Here's how the pricing actually breaks down. Some of these apps have usable free tiers, so "has a subscription" doesn't automatically mean "unusable without paying." I've noted that where it applies.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Free tier usable?&lt;/th&gt;
&lt;th&gt;Paid tier&lt;/th&gt;
&lt;th&gt;Account required?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SoulSync mood tracker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully, everything&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daylio&lt;/td&gt;
&lt;td&gt;Yes, core features&lt;/td&gt;
&lt;td&gt;Premium (paid upgrade)&lt;/td&gt;
&lt;td&gt;No (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixels (Year in Pixels)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Pixels+ paid upgrade; cloud sync paid&lt;/td&gt;
&lt;td&gt;No (optional for sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finch&lt;/td&gt;
&lt;td&gt;Yes, fully functional&lt;/td&gt;
&lt;td&gt;Finch Plus subscription&lt;/td&gt;
&lt;td&gt;No (optional for backup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bearable&lt;/td&gt;
&lt;td&gt;Yes, most features&lt;/td&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;td&gt;Yes, to use at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MindDoc (formerly Moodpath)&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few specifics, with the caveats attached:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bearable&lt;/strong&gt; charges roughly $6.99 a month or $34.99 a year at the time of writing, and it requires you to create an account before you can use it at all. To its credit, most of its features are usable free; the subscription adds the deeper analytics and correlation reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finch&lt;/strong&gt; keeps its free tier functional, and Finch Plus is a subscription in the region of $10 a month or around $70 a year, though the exact figure varies by region so check your store. Plus mostly adds extra customization rather than core tracking, which is a fairer split than most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pixels (Year in Pixels)&lt;/strong&gt; is free to use, with a paid Pixels+ upgrade for extra features and cloud sync. The core year-in-pixels grid is free and works without an account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daylio&lt;/strong&gt; is freemium with a paid Premium tier. Its exact current price is inconsistent enough across sources that I won't print a number I can't stand behind, so check the store. Worth saying: Daylio's free tier is usable and doesn't require an account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MindDoc&lt;/strong&gt;, previously Moodpath, requires an account and leans clinical, with a paid subscription for the fuller experience. Again the pricing is messy across listings, so verify it live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern across the table: the apps that insist on an account before you can start (Bearable, MindDoc) are also the ones built around a subscription and the cloud. The ones that stay closest to free (Daylio, Pixels, Finch's core) also stay closest to on-device and no-login. That's not a coincidence. Subscriptions and servers travel together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The genuinely free, no-subscription options
&lt;/h2&gt;

&lt;p&gt;If you want to skip the paywall entirely, there are two kinds of app that never bring one out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SoulSync&lt;/strong&gt; is the app I built, and it has no paid tier at all. Not "free with an upgrade," just free. The 10-point mood scale, activity and photo entries, the stats screen with trends and day-of-week patterns and activity correlation, the Insights tab, five themes, and JSON export are all in the one free app. It's open source under GPL-3.0 and stores everything locally with no account, so there's no server to justify a monthly charge and no captured data to monetise later. The absence of a subscription isn't a promotion that expires. It's structural.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open-source trackers on F-Droid&lt;/strong&gt; are the other reliably free category, because they're community projects rather than businesses. Track &amp;amp; Graph, Daily You, Mood Cairns, and moreDays are all free, all GPL-3.0, and none of them will ever show you an upgrade screen. They differ in focus (Track &amp;amp; Graph is configurable, Daily You and moreDays lean journaling, Mood Cairns is offline mood-only), and I've compared them in &lt;a href="https://raeduslabs.com/blog/best-open-source-mood-trackers-2026" rel="noopener noreferrer"&gt;the best open-source mood trackers for 2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And to be fair to the freemium apps: if you don't need the paywalled parts, Daylio, Pixels, and Finch all have free tiers you can use indefinitely without paying. "Has a subscription" and "can't be used for free" aren't the same thing. The reason to prefer a genuinely subscription-free app is that you never hit the wall at all, and you're never one policy change away from your existing features moving behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're not giving up by going free
&lt;/h2&gt;

&lt;p&gt;People assume the free option must be the worse one. For mood tracking, that assumption mostly doesn't hold, because the expensive-to-build part isn't the tracking. A free, local, open-source tracker can give you the finer mood scale, the correlations, and the full stats, since none of that costs anything to run. What a subscription actually pays for is cross-device cloud sync and a company's ongoing salary. If you don't need the sync, you're paying for the salary.&lt;/p&gt;

&lt;p&gt;The one real tradeoff is the same as with any no-account app: you run your own backup with an export instead of leaning on a synced server. That's a fair price for never paying a monthly fee, and it's covered in more depth in &lt;a href="https://raeduslabs.com/blog/mood-tracker-no-account-private" rel="noopener noreferrer"&gt;our piece on no-account, private mood tracking&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is there a completely free mood tracker with no subscription?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. SoulSync has no paid tier at all, and the open-source trackers on F-Droid (Track &amp;amp; Graph, Daily You, Mood Cairns, moreDays) are free by design. None of them will ever show you an upgrade screen, because they aren't built to sell one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I have to pay for Daylio?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, its free tier is usable and doesn't require an account. You only pay if you want Premium features. If what you want is on the paid side, &lt;a href="https://raeduslabs.com/blog/daylio-alternative-free-android" rel="noopener noreferrer"&gt;a free, open-source Daylio alternative&lt;/a&gt; keeps every feature free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do mood trackers charge a subscription at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because freemium is a strong business model, not because tracking is expensive. Logging text and numbers to a local database costs almost nothing. The subscription is priced to what a committed user will pay, not to the cost of serving them, which is exactly why you can safely skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a free mood tracker worse than a paid one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually not, for the core job. The finer scale, the stats, and the correlations are cheap to build, so a good free app includes them. Subscriptions mostly buy cloud sync and fund the company. If you don't need sync across devices, free gives up very little.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which free tracker is the most private?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The strongest combination is free, no account, on-device, and open source, so the privacy is verifiable. SoulSync fits that, as do the F-Droid apps above. A closed app's free tier can still be private, but you're trusting the policy rather than checking the code.&lt;/p&gt;




&lt;p&gt;You should not pay a monthly fee to keep a record of your own moods. If you want a tracker with every feature free, no account, and no paywall to ever hit, &lt;a href="https://raeduslabs.com/soulsync/" rel="noopener noreferrer"&gt;SoulSync&lt;/a&gt; is free on Google Play and open source on GitHub. Log your first entry in under a minute, and there's no upgrade screen waiting behind it.&lt;/p&gt;

</description>
      <category>android</category>
      <category>opensource</category>
      <category>mentalhealth</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
