<?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>Forem: Lukasz Ostrowski</title>
    <description>The latest articles on Forem by Lukasz Ostrowski (@lkostrowski).</description>
    <link>https://forem.com/lkostrowski</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2496286%2Fa3a372fb-acb8-45e8-98ab-a6f213049862.jpeg</url>
      <title>Forem: Lukasz Ostrowski</title>
      <link>https://forem.com/lkostrowski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lkostrowski"/>
    <language>en</language>
    <item>
      <title>Cleaning up large frontend codebase</title>
      <dc:creator>Lukasz Ostrowski</dc:creator>
      <pubDate>Wed, 15 Oct 2025 14:21:30 +0000</pubDate>
      <link>https://forem.com/saleor/cleaning-up-large-frontend-codebase-1ani</link>
      <guid>https://forem.com/saleor/cleaning-up-large-frontend-codebase-1ani</guid>
      <description>&lt;p&gt;Recently I started work on the new extensions functionality in Saleor Dashboard. This repo is quite large (450k LOC). I decided to make some cleanup before I introduce the feature.&lt;/p&gt;

&lt;p&gt;So the plan was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make a feature...&lt;/li&gt;
&lt;li&gt;... but first, refactor this and that....&lt;/li&gt;
&lt;li&gt;... but first, remove some dead code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;tldr: This post is about removing approx 30k LOC (~6.6%) of the codebase and bundle size (pre-gzipped) lowered by 350KB&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosis
&lt;/h2&gt;

&lt;p&gt;First of all, I have diagnosed where the main gains are. I found three main areas&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Invalid module refactor&lt;/li&gt;
&lt;li&gt;Stale feature flags&lt;/li&gt;
&lt;li&gt;Unused graphQL queries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted to focus mainly on them, because I already had some understanding what's going on. I also wanted to keep them specifically, to reduce the code complexity before my change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invalid module refactor
&lt;/h3&gt;

&lt;p&gt;Some time ago Saleor consolidated extensions model - we have merged Apps, Plugins and Webhooks under the single domain - extensions.&lt;/p&gt;

&lt;p&gt;The refactor happened using feature flag, but it was not "branching" with specific differences (like show route A or B). Instead entire module was copy pasted 1:1 to a new directory. Then for few months we maintained both of them and they slowly started to differ.&lt;/p&gt;

&lt;p&gt;The goal is to drop the old code without breaking anything&lt;/p&gt;

&lt;h3&gt;
  
  
  Stale feature flags
&lt;/h3&gt;

&lt;p&gt;We also had several feature flags, all of them quite stale. All of them enabled, but entire code branching for disabled flags still was in the source. The goal here was to drop flags and remove dead code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unused queries
&lt;/h3&gt;

&lt;p&gt;When I removed flags, I realised many graphQL queries exist in the codebase (and are processed by codegen into types and executable operations). The goal here was to remove as many of them as possible&lt;/p&gt;

&lt;h2&gt;
  
  
  Techniques
&lt;/h2&gt;

&lt;p&gt;I will not write a deep dive of my every move in this process, but rather share some learnings on the process&lt;/p&gt;

&lt;h3&gt;
  
  
  Static analysis
&lt;/h3&gt;

&lt;p&gt;During the cleanup I was using existing static analysis tool and improved their config. It's probably the best possible way to diagnose and maintain quality around dead code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ESLint - detect unused declarations, plugins like graphQL plugin can help detecting issues with queries, etc.&lt;/li&gt;
&lt;li&gt;Knip - scans the code for unused exports. Not always working, but it can show not-so-easy dependencies. For example, when code is imported by test, technically this code is not "unused". Knip can detect that&lt;/li&gt;
&lt;li&gt;Dependency cruiser - can enforce architecture decisions like forbid  circular deps etc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was using such tools on every PR to validate my changes and find new issues&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;p&gt;Obviously before the refactor, we should have good tests coverage to verify if our changes didn't break anything. &lt;/p&gt;

&lt;p&gt;With AI this is easier than ever. Code that is hard to test is often not tested. Claude wrote tests for me, I only reviewed if assertions are valid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dropping &lt;code&gt;default export&lt;/code&gt; and barrel files
&lt;/h3&gt;

&lt;p&gt;Both &lt;code&gt;export default&lt;/code&gt; and barrel files (index.js/ts) are known anti-patterns. Their existence make static analysis a nightmare. I did several runs to remove them before moving with other changes&lt;/p&gt;

&lt;h3&gt;
  
  
  Small PRs
&lt;/h3&gt;

&lt;p&gt;It's tempting to add more and more changes into the same PR, but it never works. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large PR is hard/impossible to be checked by human properly&lt;/li&gt;
&lt;li&gt;LLMS context is too low to review them as well&lt;/li&gt;
&lt;li&gt;It's not friendly to ask someone to check such code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, I tried (not always successfully, but it's something) to introduce small and cohesive changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR 1: Remove &lt;code&gt;export default&lt;/code&gt; statements and update imports → reviewer only has one "context" to verify and usually if it builds, it works&lt;/li&gt;
&lt;li&gt;PR 2: Remove barrel files and update imports → ditto&lt;/li&gt;
&lt;li&gt;PR 3: Remove bunch of files, nothing else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;etc.&lt;/p&gt;

&lt;p&gt;I broke this rule once - and on the review the bug was found. I didn't have time to fix it then, and the change was quite large. Before I was able to go back to my work, I had so many conflicts, I had to start from scratch&lt;/p&gt;

&lt;h3&gt;
  
  
  AI codemods instead of direct changes
&lt;/h3&gt;

&lt;p&gt;I realized using AI that for such a large codebase, AI often fail to keep the context. I use it to write a test suite, but I can't ask it to refactor the codebase.&lt;/p&gt;

&lt;p&gt;What I did instead was starting to use AI to write codemods and other scripts.&lt;/p&gt;

&lt;p&gt;For example, I haven't found a tool that would find unused graphQL queries (maybe because we colocate them in .ts files?). It's not easy to find dead code here, because codegen is transforming them to documents, hooks etc. So only way to find it is&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove document, rebuild codegen, build app/types and check if it's failing&lt;/li&gt;
&lt;li&gt;Automate it somehow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To automate it, I gave Claude rules how codegen is generating queries (eg &lt;code&gt;query ProductsPage&lt;/code&gt; generates &lt;code&gt;useProductsPage&lt;/code&gt; hook). It took approx 30 minutes, but it vibecoded a script that pretty much worked. Script itself can be a trash that I won't maintain, but I can use it to find what to remove, and delete it afterwards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep flags clean
&lt;/h3&gt;

&lt;p&gt;Our flags are controlled custom way, so we don't have any fancy tools to control their staleness. But we should, either use a service or introduce a process that will allow us to periodically drop them. Flags usually add major complexity and should be short living.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>refactoring</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Upgrading React 17 to 18 in a large codebase using AI</title>
      <dc:creator>Lukasz Ostrowski</dc:creator>
      <pubDate>Mon, 11 Aug 2025 06:24:11 +0000</pubDate>
      <link>https://forem.com/saleor/upgrading-react-17-18-in-large-codebase-using-ai-506j</link>
      <guid>https://forem.com/saleor/upgrading-react-17-18-in-large-codebase-using-ai-506j</guid>
      <description>&lt;p&gt;Recently, at Saleor, we &lt;a href="https://saleor.io/blog/cmd-k" rel="noopener noreferrer"&gt;invested some time into CMD+K&lt;/a&gt; (command bar) enhancements. While working on it, I realised our custom solution had become too limited and hard to maintain, so it was time to switch to an existing library.&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://github.com/saleor/saleor-dashboard" rel="noopener noreferrer"&gt;Dashboard&lt;/a&gt; - a large and mature codebase - is still running on React 17, which turned out to be a blocker because the library requires v18. You might wonder why we’re still on a five-year-old version. The answer is simple: if we don’t need a change, we prioritise other work. Until now, there was no real need to upgrade.&lt;/p&gt;

&lt;p&gt;So, the question was: is the new CMD+K enough of a reason to invest the time in upgrading?&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope of Changes
&lt;/h2&gt;

&lt;p&gt;Fortunately, changes from 17 to 18 are quite simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change how ReactDOM creates a root node (manual change, usually once per project)&lt;/li&gt;
&lt;li&gt;Upgrade types: get rid of React.FC and fix types in general.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I couldn’t find a codemod that safely migrated &lt;code&gt;React.FC&lt;/code&gt; to the shape:&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;const&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pros&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;JSX&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so I started doing this manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying an Agent
&lt;/h2&gt;

&lt;p&gt;Manually fixing these would be a monkey job, so it's time to leverage AI for help.&lt;/p&gt;

&lt;p&gt;Spoiler: it worked, but I had a few rounds of incrementing prompts:&lt;/p&gt;

&lt;h3&gt;
  
  
  Precise Prompt
&lt;/h3&gt;

&lt;p&gt;Explaining to the model what to do worked quite well, but it was too literal.&lt;/p&gt;

&lt;p&gt;For example, some components used destructuring (&lt;code&gt;{a, b, c}: Props&lt;/code&gt;), others used &lt;code&gt;props: Props&lt;/code&gt;. This mattered - in some places we passed &lt;code&gt;props&lt;/code&gt; to legacy Material UI styling via &lt;code&gt;useStyles(props)&lt;/code&gt;, so destructuring broke the flow.&lt;/p&gt;

&lt;p&gt;I had to do a few rounds to explain exactly what to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Focus on Type-Checked Files
&lt;/h3&gt;

&lt;p&gt;Instead of scanning all the files, I told the agent to run &lt;code&gt;tsc&lt;/code&gt; first and aggregate files with errors. This kept the AI focused on relevant files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Working in Batches
&lt;/h3&gt;

&lt;p&gt;Model context was clearly not enough to scan entire codebase. I explicitly asked to work in batches, clearing context in the meantime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Saving State
&lt;/h3&gt;

&lt;p&gt;At the end of each prompt, I asked to dump the “enhanced" prompt in the &lt;code&gt;.md&lt;/code&gt; file, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The original prompt&lt;/li&gt;
&lt;li&gt;Its refined version&lt;/li&gt;
&lt;li&gt;A list of files already fixed&lt;/li&gt;
&lt;li&gt;Any performance optimisations it used&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I committed these files to git to make it easy to pause, resume later, or hand off to another AI. The results were surprisingly good.&lt;/p&gt;

&lt;h3&gt;
  
  
  Picking up by another agent
&lt;/h3&gt;

&lt;p&gt;To avoid hitting usage limits, I switched between Claude Code, Atlassian's Rovo, and JetBrains Juni. Each could pick up exactly where the other left off using the saved prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live Editing vs Codegen
&lt;/h3&gt;

&lt;p&gt;While researching this topic, I noticed an interesting approach, heavily used by Juni. It created codemods instead of fixing each file directly in the following flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a transformation script&lt;/li&gt;
&lt;li&gt;Test it on one file&lt;/li&gt;
&lt;li&gt;Iterate&lt;/li&gt;
&lt;li&gt;Apply it to the whole codebase&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In future upgrades, I might skip manual batching and go straight to having AI generate a codemod.&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>refactoring</category>
      <category>frontend</category>
    </item>
    <item>
      <title>TypeScript type inference - the dark side</title>
      <dc:creator>Lukasz Ostrowski</dc:creator>
      <pubDate>Thu, 05 Jun 2025 06:47:17 +0000</pubDate>
      <link>https://forem.com/saleor/typescript-type-inference-the-dark-side-12i1</link>
      <guid>https://forem.com/saleor/typescript-type-inference-the-dark-side-12i1</guid>
      <description>&lt;p&gt;Type inference in TypeScript allows it to figure out what is the type, without explicitly declaring it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;mutableVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;// Type is number&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;immutableVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;// Type is 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can try it &lt;a href="https://www.typescriptlang.org/play/?#code/DYUwLgBAtgrmCGAjUA1EAnAzgSwPYDsIBeCARgCgBjAzSbKWBZENLPQk0oA" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;TypeScript is not only smart enough to figure out that &lt;code&gt;1&lt;/code&gt; is a number (duh) but also to understand that &lt;code&gt;const&lt;/code&gt; declaration for a primitive can't be reassigned (it's a value, not a reference) - hence it will stay &lt;code&gt;1&lt;/code&gt; forever, contrary to &lt;code&gt;let&lt;/code&gt; which can change.&lt;/p&gt;

&lt;p&gt;TypeScript tries to do it's best and usually it works quite well. Together with &lt;code&gt;satisfies&lt;/code&gt; and &lt;code&gt;as const&lt;/code&gt; statements we can write type-safe code barely declaring anything.&lt;/p&gt;

&lt;p&gt;However, there are code-architecture downsides to relying too heavily on type inference, which can eventually make maintenance difficult.&lt;/p&gt;

&lt;h1&gt;
  
  
  Code first or design first
&lt;/h1&gt;

&lt;p&gt;From my experience, most developers tend to write code and figure out the design as the outcome. "Something" eventually works, a few tests are added on top and we are done.&lt;/p&gt;

&lt;p&gt;This approach may be more satisfying but is more "artistic" than an "engineering" approach. The implementation of the logic itself is not too important, if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It works, which tests proof&lt;/li&gt;
&lt;li&gt;Is performant enough to match our metrics&lt;/li&gt;
&lt;li&gt;Is encapsulated, so it doesn't leak where it doesn't belong to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all these 3 are met, we can always easily replace the implementation without affecting the rest of the program.&lt;/p&gt;

&lt;p&gt;In the context of the design, encapsulation is critical. It draws the boundary of the abstractions (function, classes, modules) and allows us to design how they coexist and communicate.&lt;/p&gt;

&lt;p&gt;At this point, you may be thinking - what does it have in common with inference?&lt;/p&gt;

&lt;h1&gt;
  
  
  Interface vs inference
&lt;/h1&gt;

&lt;p&gt;By allowing language to infer the type, we accept it to follow a "code first" approach.&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;wrapCollection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;next&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="nx"&gt;next&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What does it return? Apart from the cognitive load (you force someone to read and understand implementation to understand what is returned), you just let TypeScript figure it out. And this is a simple function, for sure you have seen multiple-layered &lt;em&gt;map/filter/reduce&lt;/em&gt; monster, best if placed in some React component, to make testing even harder 🥲&lt;/p&gt;

&lt;p&gt;You can also be a good colleague and do this.&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;CollectionItem&lt;/span&gt; &lt;span class="o"&gt;=&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&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;type&lt;/span&gt; &lt;span class="nx"&gt;WrapCollection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CollectionItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapCollection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WrapCollection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What has changed? You started with declaring the data shape and data flow (in and out), &lt;em&gt;then&lt;/em&gt; started to implement. Even empty, not implemented functions will be ready to import and write tests at an early stage, where you can validate the API.&lt;/p&gt;

&lt;p&gt;Relying on inference is like writing only half of the interface.&lt;/p&gt;

&lt;h1&gt;
  
  
  Impact on the maintenance
&lt;/h1&gt;

&lt;p&gt;Using inference not only makes it difficult to design a good code but also makes it harder to maintain.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;wrapCollection&lt;/code&gt; in a few months can be used by many engineers in many places. They will rely on inferred type... Then you need to refactor. &lt;/p&gt;

&lt;p&gt;Say, you want to change it to &lt;code&gt;for&lt;/code&gt; instead of &lt;code&gt;reduce&lt;/code&gt; because you operate on a large amount of data and need to &lt;a href="https://leanylabs.com/blog/js-forEach-map-reduce-vs-for-for_of/" rel="noopener noreferrer"&gt;improve performance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You change the inner implementation, &lt;em&gt;but there is no Typescript boundary preventing you from changing the outer shape&lt;/em&gt;. Yes, the rest of the code hopefully will be "red" and your tests will fail. But there are many codebases, including ones with not full TypeScript coverage and missing tests.&lt;/p&gt;

&lt;p&gt;But every time, your function defines its outer shape (doesn't have to be an interface, can be just a static declaration of the returned type), you are locally protected from breaking that contract.&lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;I'm not declaring types literally everywhere, but I believe strong type coverage makes code more maintainable. &lt;/p&gt;

&lt;p&gt;The more complex the function is, the higher the ROI is. Simple, especially private functions are not that important if we know that only one caller exists. But public methods, widely used across the codebase, benefit from being typed.&lt;/p&gt;

&lt;p&gt;You can use &lt;a href="https://typescript-eslint.io/rules/explicit-function-return-type/" rel="noopener noreferrer"&gt;ESLint rule&lt;/a&gt; to require explicit function return type as well.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Error modelling</title>
      <dc:creator>Lukasz Ostrowski</dc:creator>
      <pubDate>Thu, 29 May 2025 06:52:24 +0000</pubDate>
      <link>https://forem.com/saleor/error-modelling-4471</link>
      <guid>https://forem.com/saleor/error-modelling-4471</guid>
      <description>&lt;h1&gt;
  
  
  Intro
&lt;/h1&gt;

&lt;p&gt;Let’s have a conceptual look at error modeling. I will use Node.js ecosystem and TypeScript in these examples (and some pseudo-code). I find built-in error management in the JS ecosystem rather poor compared to some other languages, which makes it even more important to treat this topic seriously in this tech stack&lt;/p&gt;

&lt;p&gt;I focus on the error modeling, but I don’t focus on the data flow. In another article, I will write more about managing errors Rust-way (which has the built-in distinction between recoverable and non-recoverable errors)&lt;/p&gt;

&lt;h1&gt;
  
  
  Role of errors
&lt;/h1&gt;

&lt;p&gt;Let’s think about all of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SyntaxError: JSON.parse: unexpected character&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TypeError: Cannot read property 'value' of null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ValidationError: Email already exists&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Internal Server Error&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of them is an error with a different origin and reason.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;JSON.parse&lt;/code&gt; will happen when we try to parse a string that is not valid JSON. It can occur when we directly catch the external API response body and without checking the response type, we parse e.g. error page which is HTML&lt;/p&gt;

&lt;p&gt;Another &lt;code&gt;TypeError&lt;/code&gt; can be a pure static code issue - we can try to access a property of nullish value, for example, accessing an object property before it has been created.&lt;/p&gt;

&lt;p&gt;Common things for these two is that they are mainly useful for the developer. Best if they are caught during compilation or static analysis if possible, then we can protect ourselves by writing proper tests. Once we reach the runtime, we must ensure we will be able to recognize them when the application crashes - in logs or error-tracking platforms like Sentry. These errors are also often non-recoverable - the app probably can’t find another way to work if the code can’t execute anymore.&lt;/p&gt;

&lt;p&gt;Once we reach &lt;code&gt;ValidationError&lt;/code&gt; we change the abstraction level. First of all, it’s not language that will throw such errors, but either our database or our internal data layer that is trying e.g. to insert a user into the database. Validation is a graceful way to recover from the issue, giving clear feedback without crashing the app. Such an error is also different from the previous two: it’s rather not interesting to track it in the error tracker (it’s not something we can fix) and this error should be returned in the response, so the user/frontend can handle it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Internal Server Error&lt;/code&gt; on the other hand is an error on the HTTP layer. It’s represented by the semantic code (500) but also provides information that it’s a crash on the server side. Something we definitely should catch and fix. We definitely need as many details as possible in our internal tracking systems, but also do not expose any detail to the front end to avoid leaking any implementation details.&lt;/p&gt;

&lt;p&gt;You can see now, that errors are not equal to errors - depending on the context they differ. And for that reason, it requires us to model errors carefully.&lt;/p&gt;

&lt;h1&gt;
  
  
  Abstraction is the problem
&lt;/h1&gt;

&lt;p&gt;Conceptually errors can propagate through the stack trace:&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="nc"&gt;FunctionA&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nc"&gt;FunctionB&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;FunctionC&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stack trace follows the function execution. Then, the opposite when we are catching it:&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;FunctionA&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="nc"&gt;FunctionB&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="nc"&gt;FunctionC&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="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;The inner error will be traveling through the execution until some &lt;code&gt;catch&lt;/code&gt; block intercepts it (or the program finishes with the unhandled exception).&lt;/p&gt;

&lt;p&gt;Now when we think about it in scale - a program running hundreds of functions to process the request, we travel through the abstraction layers. &lt;/p&gt;

&lt;p&gt;Errors that arise in the controllers can be often caused by validation logic, partially represented business rules (minimal password length), partially expected data format (JSON), etc. &lt;/p&gt;

&lt;p&gt;Errors that are caught in the model are likely related strictly to the domain, for example, we can’t add to a cart product that doesn’t exist anymore.&lt;/p&gt;

&lt;p&gt;Sometimes, errors happen in external services. Can be our database downtime or external API not responding.&lt;/p&gt;

&lt;p&gt;And in every other place, we can face dozens of programming errors, caused by wrong implementation on the language level.&lt;/p&gt;

&lt;p&gt;Depending on the abstraction we will need a different handling &lt;/p&gt;

&lt;h1&gt;
  
  
  Errors chaining
&lt;/h1&gt;

&lt;p&gt;In Python, there is a concept of &lt;code&gt;raise ErrorA from ErrorB&lt;/code&gt;. Its purpose is to indicate one error is caused by another, especially useful when we transform errors.&lt;/p&gt;

&lt;p&gt;In the JS stack, we can use &lt;code&gt;Error.prototype.cause&lt;/code&gt; for that.&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;// not real API&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stripeError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stripeError&lt;/span&gt; &lt;span class="nx"&gt;instanceOf&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InvalidCardDetails&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;PaymentFailedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment failed to due invalid payment details&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stripeError&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;This is a powerful pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, we have locally intercepted errors from the 3rd party API. It gives us control, at this point, we can log, track, set metrics, emit events, or anything else.&lt;/li&gt;
&lt;li&gt;Second, we can match the error type. External APIs will provide us a unified errors layer (likely HTTP serialized errors if a list of enum codes) - some of them can be recoverable, some not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An error like “invalid card details” is expected and recoverable - the user must try again.&lt;/p&gt;

&lt;p&gt;An error like “invalid Stripe secret key” is not an action for the customer, but definitely should reach the payment operator to fix it, otherwise, the business critical path may be down. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Third, we still chain the reasons, allowing us to track not only the stack tree (representing function calls) but also the human-readable messages we wrote in our code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we move level up from the payment example above, we will be able to see simplified reasoning of how powerful error matching is:&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;// controller / app service / use case&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="nx"&gt;paymentProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pay&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;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
  &lt;span class="k"&gt;switch&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;case&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nx"&gt;instanceOf&lt;/span&gt; &lt;span class="nx"&gt;PaymentFailedError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;// In real life respond with a json-like structure with a payment refusal reason&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid payment details&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nx"&gt;instanceOf&lt;/span&gt; &lt;span class="nx"&gt;GatewayAuthError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;captureException&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;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;Payment Gateway auth rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FATAL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error processing payment, please try a different payment method&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;In real life, it will be much broader, due to all handled error cases - but conceptually, we can achieve the same thing: a strong and clear distinction between what class of error we are dealing with, who should receive it, what data we provide and how do we monitor these.&lt;/p&gt;

&lt;p&gt;Passing cause gives us additional context we can log (e.g. into Sentry), but we don’t have to return it to the storefront. Or maybe we want to, but only for test environments (we do that in our Stripe App in Saleor)&lt;/p&gt;

&lt;p&gt;Error matching (that can be implemented with extending Errors or by enum-like reasons) allows us to route error handling and react properly depending on the issue.&lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Try to think about what types of errors your application can produce&lt;/li&gt;
&lt;li&gt;Model what data you need to attach to your errors (both internal and external systems)&lt;/li&gt;
&lt;li&gt;Leverage the &lt;code&gt;Error.prototype.cause&lt;/code&gt; field to chain errors&lt;/li&gt;
&lt;li&gt;Transform errors when they travel through the abstraction layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the upcoming article, I will write more about error implementation and error flow in the application.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>node</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
