<?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: Chloe Zhou</title>
    <description>The latest articles on DEV Community by Chloe Zhou (@czhoudev).</description>
    <link>https://dev.to/czhoudev</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%2F1841973%2Ffa655372-9425-435d-934a-ca4d0a913e27.jpg</url>
      <title>DEV Community: Chloe Zhou</title>
      <link>https://dev.to/czhoudev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/czhoudev"/>
    <language>en</language>
    <item>
      <title>Why My Model Wouldn’t Deploy to Hugging Face Spaces (and What Git LFS Actually Does)</title>
      <dc:creator>Chloe Zhou</dc:creator>
      <pubDate>Sun, 11 Jan 2026 02:02:10 +0000</pubDate>
      <link>https://dev.to/czhoudev/why-my-model-wouldnt-deploy-to-hugging-face-spaces-and-what-git-lfs-actually-does-9kf</link>
      <guid>https://dev.to/czhoudev/why-my-model-wouldnt-deploy-to-hugging-face-spaces-and-what-git-lfs-actually-does-9kf</guid>
      <description>&lt;p&gt;I trained a simple &lt;strong&gt;“is cat” image classifier&lt;/strong&gt; using &lt;a href="https://docs.fast.ai/quick_start.html" rel="noopener noreferrer"&gt;fastai&lt;/a&gt; and wanted to deploy a small demo on &lt;a href="https://huggingface.co/spaces" rel="noopener noreferrer"&gt;Hugging Face Spaces&lt;/a&gt;. I already had a working &lt;code&gt;app.py&lt;/code&gt; and a trained &lt;code&gt;model.pkl&lt;/code&gt;, so my plan felt straightforward: commit everything and push it to the remote Hugging Face repository.&lt;/p&gt;

&lt;p&gt;At that point, I thought the hard part was already over. The model was trained, the demo worked locally, and deployment felt like it should be a routine step — commit, push, done.&lt;/p&gt;

&lt;p&gt;That assumption didn’t last long.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Tried (and Why It Kept Failing)
&lt;/h3&gt;

&lt;p&gt;When I tried to push the repository, Git rejected it with the following error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;remote: &lt;span class="nt"&gt;-------------------------------------------------------------------------&lt;/span&gt;
remote: Your push was rejected because it contains files larger than 10 MiB.
remote: Please use https://git-lfs.github.com/ to store large files.
remote: See also: https://hf.co/docs/hub/repositories-getting-started#terminal
remote:
remote: Offending files:
remote:   - model.pkl &lt;span class="o"&gt;(&lt;/span&gt;ref: refs/heads/main&lt;span class="o"&gt;)&lt;/span&gt;
remote: &lt;span class="nt"&gt;-------------------------------------------------------------------------&lt;/span&gt;
To https://huggingface.co/spaces/chloezhoudev/minima
 &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;remote rejected] main -&amp;gt; main &lt;span class="o"&gt;(&lt;/span&gt;pre-receive hook declined&lt;span class="o"&gt;)&lt;/span&gt;
error: failed to push some refs to &lt;span class="s1"&gt;'https://huggingface.co/spaces/chloezhoudev/minima'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could see what Git was complaining about — &lt;code&gt;model.pkl&lt;/code&gt; was too large — but I didn’t really understand &lt;em&gt;why&lt;/em&gt; this was a problem, or what “&lt;a href="https://git-lfs.com/" rel="noopener noreferrer"&gt;Git LFS&lt;/a&gt;” actually meant in practice. I had never used it before.&lt;/p&gt;

&lt;p&gt;So I followed the instructions in the error message and tried to fix it step by step.&lt;/p&gt;

&lt;p&gt;First, I installed Git LFS and set it up locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;git-lfs    &lt;span class="c"&gt;# Install Git LFS (macOS via Homebrew)&lt;/span&gt;
git lfs &lt;span class="nb"&gt;install&lt;/span&gt;         &lt;span class="c"&gt;# Initialize Git LFS and register Git hooks&lt;/span&gt;
git lfs version         &lt;span class="c"&gt;# Confirm Git LFS installation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I told Git LFS to track my model file and committed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git lfs track &lt;span class="s2"&gt;"model.pkl"&lt;/span&gt;
git add model.pkl
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Track model file with Git LFS"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The push failed again — with the &lt;strong&gt;exact same error&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At this point, I was getting annoyed. Instead of really understanding how Git LFS worked, I tried to brute-force my way through the problem and asked ChatGPT for help. It suggested running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--cached&lt;/span&gt; model.pkl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then tracking the file with Git LFS again and committing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git lfs track &lt;span class="s2"&gt;"model.pkl"&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Store model file using Git LFS"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third attempt — same error. 😅&lt;/p&gt;

&lt;p&gt;At this point, I seriously considered giving up. But since I already understood &lt;em&gt;what&lt;/em&gt; I wanted to do, I felt I should at least understand &lt;em&gt;why&lt;/em&gt; this wasn’t working. So I went back to ChatGPT one more time, and this time it suggested something very different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;--orphan&lt;/span&gt; clean-main
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial deployment with Git LFS model"&lt;/span&gt;

git branch &lt;span class="nt"&gt;-M&lt;/span&gt; main
git push &lt;span class="nt"&gt;-f&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, the push finally worked.&lt;/p&gt;

&lt;p&gt;Now that everything was deployed successfully, it was time to stop guessing and actually understand what had happened:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why did Git keep rejecting my pushes?&lt;br&gt;&lt;br&gt;
What does Git LFS &lt;em&gt;actually&lt;/em&gt; do?&lt;br&gt;&lt;br&gt;
Why didn’t &lt;code&gt;git rm --cached&lt;/code&gt; help?&lt;br&gt;&lt;br&gt;
And why did creating an orphan branch fix everything?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What I Eventually Learned About Git and Git LFS
&lt;/h3&gt;

&lt;p&gt;The first problem was that I didn’t actually understand &lt;em&gt;why&lt;/em&gt; my push was rejected. Reading the error alone wasn’t enough — I just saw a message telling me to use Git LFS. It wasn’t until later that I learned Hugging Face repos enforce a &lt;strong&gt;pre-receive hook&lt;/strong&gt; on their side that scans &lt;em&gt;all commits being push to the remote&lt;/em&gt; (any commits not already on Hugging Face's servers) and &lt;strong&gt;rejects the push if any commit contains a file larger than 10 MiB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once that clicked, I began to think about how Git actually stores files and what role Git LFS plays. The simplest way to think about it is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A normal Git repository stores file contents as &lt;strong&gt;blob objects&lt;/strong&gt; — each file you add is stored roughly at its original size in the history. For binary files, this is exactly what happens: Git takes the content and writes a blob object containing that data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Git LFS replaces large files with &lt;strong&gt;pointer files&lt;/strong&gt; and stores the actual big file contents separately on a dedicated LFS server. When you git add a file while Git LFS is enabled, Git LFS generates a small pointer and hands that pointer file over to Git to store in the repository. The large file itself gets uploaded to the Git LFS store instead.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So in my case, &lt;code&gt;model.pkl&lt;/code&gt; was a large binary file.&lt;br&gt;&lt;br&gt;
Without Git LFS installed &lt;em&gt;before it was ever added&lt;/em&gt;, Git simply stored it as a normal blob at full size.&lt;br&gt;&lt;br&gt;
That’s why my first push was rejected — the blob itself exceeded Hugging Face’s 10 MiB limit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next piece of the puzzle was the &lt;code&gt;.gitattributes&lt;/code&gt; file that Hugging Face includes automatically when you create a Space repository. By default it contains a line like this:&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="k"&gt;*&lt;/span&gt;.pkl &lt;span class="nv"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lfs &lt;span class="nv"&gt;diff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lfs &lt;span class="nv"&gt;merge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lfs &lt;span class="nt"&gt;-text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line tells Git which files &lt;strong&gt;should&lt;/strong&gt; be tracked by Git LFS instead of by normal Git. However, for this to work, you must have Git LFS installed &lt;em&gt;and&lt;/em&gt; initialized (&lt;code&gt;git lfs install&lt;/code&gt;) &lt;strong&gt;before&lt;/strong&gt; adding the file. Since I didn't have Git LFS set up when I first committed &lt;code&gt;model.pkl&lt;/code&gt;, that rule had no effect.&lt;/p&gt;

&lt;p&gt;Which brings us to the next question: what happened on the second push?&lt;/p&gt;

&lt;p&gt;Even after I installed Git LFS and tracked the model file, the push was rejected again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Installing Git LFS does not fix files that were already committed.&lt;br&gt;&lt;br&gt;
The old commit containing &lt;code&gt;model.pkl&lt;/code&gt; as a normal blob was still in the history, and Hugging Face rejected the push because of it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What about &lt;code&gt;git rm — cached&lt;/code&gt;? &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git rm --cached&lt;/code&gt; only affects future commits.&lt;br&gt;
It does &lt;strong&gt;not&lt;/strong&gt; remove files from your existing Git history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The command removes the file from the staging area and stops tracking it in upcoming commits, while leaving the file intact in your working directory. However, it does nothing to delete the earlier commit where the large file was first added.&lt;/p&gt;

&lt;p&gt;Because that original commit still contained &lt;code&gt;model.pkl&lt;/code&gt; as a normal Git blob, the problematic file remained in the repository history — and Hugging Face continued to reject the push.&lt;/p&gt;

&lt;p&gt;Finally, creating a new branch with no history (&lt;code&gt;git checkout — orphan&lt;/code&gt;) fixed everything because it &lt;strong&gt;started with a clean slate&lt;/strong&gt; that had no commits at all. &lt;/p&gt;

&lt;p&gt;Once I added the files with Git LFS already configured, committed them, renamed &lt;code&gt;clean-main&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; (using &lt;code&gt;git branch -M main&lt;/code&gt;), and force-pushed, the remote accepted it. There were no old blob objects in the history for the pre-receive hook to reject.&lt;/p&gt;

&lt;p&gt;One more warning: using &lt;code&gt;— orphan&lt;/code&gt; and especially &lt;code&gt;git push -f&lt;/code&gt; is dangerous if multiple collaborators are using the same branch, because this &lt;strong&gt;permanently deletes all previous commits&lt;/strong&gt; from the remote and can break everyone else's local copies. In my case, the Space repo was just for deployment, so this was fine, but it’s something to be careful about in team settings.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Bonus: Git LFS isn’t the only option anymore&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hugging Face now also recommends &lt;strong&gt;&lt;a href="https://huggingface.co/docs/hub/en/xet/index" rel="noopener noreferrer"&gt;git-xet&lt;/a&gt;&lt;/strong&gt;, a newer backend designed specifically for large machine learning artifacts.&lt;/p&gt;

&lt;p&gt;Both Git LFS and git-xet store large file content separately from Git's object database. The difference is that Git LFS stores complete files, while git-xet uses chunking and deduplication to handle incremental changes more efficiently — particularly useful for ML models that evolve over time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After all that debugging, the model is finally deployed and working as expected.&lt;/p&gt;

&lt;p&gt;You can try the live demo &lt;a href="https://huggingface.co/spaces/chloezhoudev/minima" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If this helped you, I write about real dev problems at &lt;a href="https://chloezhou.dev" rel="noopener noreferrer"&gt;chloezhou.dev&lt;/a&gt; — you can subscribe to get new posts by email.&lt;/p&gt;

</description>
      <category>git</category>
      <category>gitlfs</category>
      <category>huggingface</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>A Real Performance Bug I Found and Fixed — Step by Step</title>
      <dc:creator>Chloe Zhou</dc:creator>
      <pubDate>Sun, 27 Jul 2025 00:34:15 +0000</pubDate>
      <link>https://dev.to/czhoudev/a-real-performance-bug-i-found-and-fixed-step-by-step-348</link>
      <guid>https://dev.to/czhoudev/a-real-performance-bug-i-found-and-fixed-step-by-step-348</guid>
      <description>&lt;p&gt;A few days ago, while working on a bug at work, I noticed something odd: after I fixed the bug, the default client name value in a dropdown component took almost 2 to 3 seconds to appear after the page loaded. That kind of lag is probably unacceptable to users, so I know I had to dig deeper.&lt;/p&gt;

&lt;p&gt;Turns out, it was a performance issue.&lt;/p&gt;

&lt;p&gt;We always hear advice like “don’t optimize prematurely” or “only solve performance problems when they exist.” Well, this was the first time I actually ran into one myself — and I think the debugging process is worth documenting.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk through how I tracked down the cause of the slowdown. I can’t share the actual code or logs because of company policy, but I’ll reconstruct the process using pseudocode and reasoning steps. If you’re a frontend developer wondering how to approach a performance issue in a React app, I hope this gives you something concrete to take away.&lt;/p&gt;

&lt;h2&gt;
  
  
  If it’s not the API call, what is it?
&lt;/h2&gt;

&lt;p&gt;When I first noticed the lag in how the client name appeared, my instinct was to check the &lt;strong&gt;Network&lt;/strong&gt; tab. I opened DevTools and placed the network panel side-by-side with the UI so I could watch what was happening in real time. Sure enough, the API call to fetch client name came back quickly with a 200 OK. But even though the data returned fast, the client name still appeared in the dropdown field with a noticeable delay.&lt;/p&gt;

&lt;p&gt;That made me pause — if it’s not the network latency, then what is? Could it be something in the frontend rendering cycle? Maybe &lt;a href="https://formik.org/" rel="noopener noreferrer"&gt;Formik&lt;/a&gt;? Maybe &lt;a href="https://react.dev/learn/scaling-up-with-reducer-and-context" rel="noopener noreferrer"&gt;context&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;Here’s a bit of background. The client name is fetched from an API call and saved into the &lt;code&gt;userInfo&lt;/code&gt; object, which lives in a global context. Formik pulls its initial values from this context. But initial values alone aren’t reactive — they don’t update just because the context does. So after the context gets the latest client name, I also call &lt;code&gt;setFieldValue&lt;/code&gt; to make sure the form reflects the updated value.&lt;/p&gt;

&lt;p&gt;At this point, I suspected the delay might be happening somewhere in that chain — from receiving the data to updating the form. So the question became: &lt;strong&gt;where exactly is the bottleneck?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying data flow from API to form
&lt;/h2&gt;

&lt;p&gt;My next step was to check how state updates propagated after the API call. The question was: after updating the context with the new client name, how exactly does that value reach the Formik form? I suspected a &lt;code&gt;useEffect&lt;/code&gt; inside the page component (called Summary) might be responsible for syncing the values. So I added logs to verify the data flow step by step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Log inside Summary’s useEffect to confirm form syncing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Summary] dealInfo.clientName effect triggered:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dealInfo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;clientName&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;dealInfo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;clientName&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setFieldValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary.clientName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dealInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientName&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;span class="nx"&gt;dealInfo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;clientName&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This log shows when the &lt;code&gt;dealInfo.clientName&lt;/code&gt; value changes and triggers the code that updates the Formik field. It helps verify whether the effect runs immediately after the context updates — or if there’s any unexpected delay.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Log right after receiving the API response
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;clientName&lt;/code&gt; comes from an API call. Once the response is received, I update the global context via &lt;code&gt;setDealInfo&lt;/code&gt;. To confirm that part was working correctly, I added a log right before updating the context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setDealInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&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="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clientName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// from API response&lt;/span&gt;
  &lt;span class="p"&gt;};&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[setDealInfo] triggered with:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updated&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;updated&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 shows exactly when the client name arrives from the backend and enters the shared state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Here’s what I saw:
&lt;/h3&gt;

&lt;p&gt;On initial render, the useEffect ran quickly — but &lt;code&gt;clientName&lt;/code&gt; was still empty, so &lt;code&gt;setFieldValue&lt;/code&gt; was not called.&lt;/p&gt;

&lt;p&gt;Then nothing happened for about 2–3 seconds.&lt;/p&gt;

&lt;p&gt;After that delay, &lt;strong&gt;both&lt;/strong&gt; the &lt;code&gt;setDealInfo&lt;/code&gt; log and the &lt;code&gt;useEffect&lt;/code&gt; log appeared almost at the same time.&lt;/p&gt;

&lt;p&gt;The dropdown updated &lt;strong&gt;immediately&lt;/strong&gt; after that.&lt;/p&gt;

&lt;p&gt;This clearly shows that the 2–3 second delay was not due to slow state propagation or form logic. The delay happened &lt;strong&gt;before&lt;/strong&gt; the context or form had any chance to react — meaning:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;the bottleneck was simply the API call being slow to return.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the API Call Duration Precisely
&lt;/h2&gt;

&lt;p&gt;Now that I suspect the API call might be slow, the next step is to confirm this with accurate timing logs.&lt;/p&gt;

&lt;p&gt;I added these logs inside the &lt;code&gt;loadClientDetails&lt;/code&gt; function — the one that calls the backend API to fetch client name:&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[loadClientDetails] API call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/clientDetails&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;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;externalId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&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;timeEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[loadClientDetails] API call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setDealInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&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="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clientName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[setDealInfo] triggered with:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updated&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;updated&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 lets me measure exactly how long the API request takes from start to finish.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I saw:
&lt;/h3&gt;

&lt;p&gt;I ran the test 4 times, and the results varied wildly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;About 4000 ms (4 seconds)&lt;/li&gt;
&lt;li&gt;About 8000 ms (8 seconds)&lt;/li&gt;
&lt;li&gt;About 15000 ms (15 seconds)&lt;/li&gt;
&lt;li&gt;About 1000 ms (1 second)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This confirmed without doubt that the API is the bottleneck and sometimes responds very slowly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Fix: Show Loading Clearly, Don’t Wait in Confusion
&lt;/h2&gt;

&lt;p&gt;The simplest and most effective fix was to explicitly handle the loading state on the Summary page.&lt;/p&gt;

&lt;p&gt;Since the page already had a loading spinner for the segment data (fetched via &lt;a href="https://tanstack.com/query/v5/docs/framework/react/reference/useQuery" rel="noopener noreferrer"&gt;useQuery&lt;/a&gt;), I added a similar one for the client name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segmentIsLoading&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoadingSpinner&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading segment...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="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;dealInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientName&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoadingSpinner&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading client name...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, users immediately see that data is loading — instead of staring at an empty field, wondering if something’s broken.&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.amazonaws.com%2Fuploads%2Farticles%2Fmf8n213oh2j7rfhpwsq2.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fmf8n213oh2j7rfhpwsq2.gif" alt="A small loading indicator makes a big difference in clarity." width="967" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ Problem solved. Happy debugging — and happy summer. ☀️&lt;/p&gt;

&lt;p&gt;If this helped you, I write about real dev problems at &lt;a href="https://chloezhou.dev" rel="noopener noreferrer"&gt;chloezhou.dev&lt;/a&gt; — you can subscribe to get new posts by email.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webperf</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Data Migration Design in a CLI Tool: From Local JSON to Cloud Database</title>
      <dc:creator>Chloe Zhou</dc:creator>
      <pubDate>Sat, 05 Jul 2025 07:51:32 +0000</pubDate>
      <link>https://dev.to/czhoudev/data-migration-design-in-a-cli-tool-from-local-json-to-cloud-database-5ak4</link>
      <guid>https://dev.to/czhoudev/data-migration-design-in-a-cli-tool-from-local-json-to-cloud-database-5ak4</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I built a simple &lt;a href="https://www.npmjs.com/package/czhou-notes-cli" rel="noopener noreferrer"&gt;CLI tool&lt;/a&gt; that lets users take notes from the terminal. In version 1, there was no login and no cloud — notes were just saved to a local JSON file on the user’s computer.&lt;/p&gt;

&lt;p&gt;That setup worked fine at the beginning. But as the tool grew, I added basic authentication and moved storage to the cloud (using &lt;a href="https://supabase.com/" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;), so users could access their notes across machines.&lt;/p&gt;

&lt;p&gt;That change introduced a new problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about users who already had notes saved locally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I just switched systems without thinking it through, they’d open the new version and find their notes gone.&lt;/p&gt;

&lt;p&gt;This post explains how I handled that: designing a migration system that moves notes from the local file to the cloud — safely, clearly, and without making a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Means Moving Houses
&lt;/h2&gt;

&lt;p&gt;When I was trying to understand how to approach migration, I asked AI for help. It gave me a useful analogy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“It’s like moving houses.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You have stuff in your old home — your local JSON file — and you want to move it into a new one — your cloud database. That sounds simple, but moving isn’t just about copying boxes from one place to another. It’s about making sure everything still works in the new place, nothing important gets lost, and the move itself doesn’t break anything.&lt;/p&gt;

&lt;p&gt;Here’s what that actually means.&lt;/p&gt;

&lt;h3&gt;
  
  
  It’s not just copying — it’s cleaning up
&lt;/h3&gt;

&lt;p&gt;In version 1, all notes were saved in a single JSON file on the user’s machine. That file might contain empty notes, malformed entries, or slightly inconsistent formatting.&lt;/p&gt;

&lt;p&gt;But version 2 uses a structured cloud database (Supabase), where everything needs to follow a fixed schema. So before I can move anything, I need to &lt;strong&gt;sanitize&lt;/strong&gt; the data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removing notes that are clearly empty or invalid&lt;/li&gt;
&lt;li&gt;Making sure each note is shaped like an object with a &lt;code&gt;content&lt;/code&gt; field, and a &lt;code&gt;tags&lt;/code&gt; field, which is an array&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, before I move in, I clean the data up.&lt;/p&gt;

&lt;h3&gt;
  
  
  The data’s meaning also changes
&lt;/h3&gt;

&lt;p&gt;In version 1, the tool assumed that &lt;strong&gt;one computer = one user&lt;/strong&gt;. Notes were tied to the machine.&lt;/p&gt;

&lt;p&gt;In version 2, notes are tied to a &lt;strong&gt;cloud user account&lt;/strong&gt;. Now, you can log in from anywhere and access your notes. That’s a big shift in meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same note is now “owned” by a user, not a device&lt;/li&gt;
&lt;li&gt;Access is controlled by authentication, not file access&lt;/li&gt;
&lt;li&gt;Notes can now live across machines, not just one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So part of the migration is not just “moving files,” but &lt;strong&gt;changing what the data represents&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Users need to know what’s happening
&lt;/h3&gt;

&lt;p&gt;You don’t move into a new house without telling your friends. Migration needs to be transparent too.&lt;/p&gt;

&lt;p&gt;If I silently switch to the cloud without warning, users might open the new version and think all their notes are gone. That’s bad UX. So I designed the migration to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A check to see if old notes exist&lt;/li&gt;
&lt;li&gt;A prompt letting users choose whether to migrate&lt;/li&gt;
&lt;li&gt;Clear feedback on what was migrated and what wasn’t&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good UX means &lt;strong&gt;telling users what’s going on&lt;/strong&gt;, not making them guess.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bringing it together
&lt;/h3&gt;

&lt;p&gt;Migration isn’t just a file transfer. It’s three things working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data transformation&lt;/strong&gt;: Cleaning and reshaping the notes so they can live in a database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State change&lt;/strong&gt;: Notes now belong to a cloud account, not just a local machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User experience&lt;/strong&gt;: Letting users know what’s happening and giving them control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s what it really means to move from version 1 to version 2. It’s not just a new backend — it’s a change in what data is, who owns it, and how users interact with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration Strategy: Five Steps
&lt;/h2&gt;

&lt;p&gt;Once I understood what this migration really meant, I broke the process down into five concrete steps. Each step solves a specific problem, and together, they ensure the user’s data moves safely from version 1 to version 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detection — Does the user have old data?
&lt;/h3&gt;

&lt;p&gt;Before running any migration, I need to know whether there’s anything to migrate.&lt;/p&gt;

&lt;p&gt;The version 1 CLI tool stored notes locally in a known file path. So the first step is to check if that file exists on the user’s machine.&lt;/p&gt;

&lt;p&gt;If the file isn’t there, then this user is either new or never saved anything — no migration needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safety — Make a backup before touching anything
&lt;/h3&gt;

&lt;p&gt;Even if migration is simple, data loss is never acceptable.&lt;/p&gt;

&lt;p&gt;Before doing anything else, I make a full backup of the original JSON file. This way, if something goes wrong during migration — or if the user just wants to revert — they can recover their data.&lt;/p&gt;

&lt;p&gt;This is a simple but important rule: &lt;strong&gt;never destroy the original source&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Translation — Clean up and reshape the data
&lt;/h3&gt;

&lt;p&gt;The local notes file was flexible. In version 2, the data needs to follow a specific structure to be accepted by the cloud database.&lt;/p&gt;

&lt;p&gt;So before inserting anything, I sanitize each note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove empty notes (e.g. whitespace-only)&lt;/li&gt;
&lt;li&gt;Skip notes with missing or malformed fields&lt;/li&gt;
&lt;li&gt;Make sure each note has the expected shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the “translation” step: same ideas, but restructured to fit the new format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transfer — Save the cleaned notes to the cloud
&lt;/h3&gt;

&lt;p&gt;After cleanup, I use the database utility functions I already built (like &lt;code&gt;createNote&lt;/code&gt;) to send each note to Supabase. These functions are the same ones used by the app during normal use — so the migrated notes behave exactly like new ones.&lt;/p&gt;

&lt;p&gt;If any note fails to save, I don’t crash the whole process. I log it, skip it, and continue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verification — Show the result to the user
&lt;/h3&gt;

&lt;p&gt;Once the migration runs, I want to tell the user exactly what happened.&lt;/p&gt;

&lt;p&gt;So I track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many notes were found&lt;/li&gt;
&lt;li&gt;How many were skipped&lt;/li&gt;
&lt;li&gt;How many were successfully migrated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI shows a short summary at the end, so the user knows whether everything worked — or if they need to take a closer look.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this approach works
&lt;/h3&gt;

&lt;p&gt;This strategy covers both data integrity and user experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each step is small, focused, and testable&lt;/li&gt;
&lt;li&gt;If anything fails, users don’t lose their data&lt;/li&gt;
&lt;li&gt;Users are never left guessing what just happened&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  UX Considerations
&lt;/h2&gt;

&lt;p&gt;I didn’t want the migration to interrupt users. If they don’t have old data, it should just be invisible.&lt;/p&gt;

&lt;p&gt;If they do, they should get a clear path — but only when they need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  It only checks once, during setup
&lt;/h3&gt;

&lt;p&gt;When the user runs &lt;code&gt;note setup&lt;/code&gt;, the CLI checks if there’s a local JSON file from version 1.&lt;/p&gt;

&lt;p&gt;If it exists, the CLI shows this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Legacy notes detected!
You can run:
  notes migrate check   &lt;span class="c"&gt;# See what can be migrated&lt;/span&gt;
  notes migrate         &lt;span class="c"&gt;# Perform the migration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there’s no local data, nothing happens. No prompt, no message.&lt;/p&gt;

&lt;p&gt;This avoids showing unnecessary stuff to new users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output is simple and direct
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;note migrate&lt;/code&gt; runs, the CLI prints something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Found 8 &lt;span class="nb"&gt;local &lt;/span&gt;notes.
6 migrated successfully.
2 skipped &lt;span class="o"&gt;(&lt;/span&gt;empty or invalid&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn’t hide anything. If a note fails, it’s listed and skipped.&lt;/p&gt;

&lt;p&gt;The point is to tell users exactly what was moved, and what wasn’t.&lt;/p&gt;

&lt;h3&gt;
  
  
  It won’t run again once migration is done
&lt;/h3&gt;

&lt;p&gt;After a successful migration, the CLI deletes the old JSON file and archives a copy in a separate folder.&lt;/p&gt;

&lt;p&gt;So if the user runs &lt;code&gt;note migrate&lt;/code&gt; again, the tool won’t find anything to migrate — it just says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;No legacy notes found. Nothing to migrate.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s no per-note tracking. Migration is a one-time thing.&lt;/p&gt;

&lt;p&gt;If something goes wrong, the backup is still there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Principles Behind Good Migrations
&lt;/h2&gt;

&lt;p&gt;When I put together this migration logic, I mostly followed a few basic rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Back up first
&lt;/h3&gt;

&lt;p&gt;Before doing anything, the CLI makes a copy of the notes file (e.g. &lt;code&gt;db-backup.json&lt;/code&gt; in the same folder. If something breaks, the original file is still there.&lt;/p&gt;

&lt;h3&gt;
  
  
  If something fails, keep going
&lt;/h3&gt;

&lt;p&gt;One bad note shouldn’t block everything.&lt;/p&gt;

&lt;p&gt;The CLI skips anything invalid, and finishes what it can.&lt;/p&gt;

&lt;p&gt;No need to roll back or crash the whole thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Say what’s happening
&lt;/h3&gt;

&lt;p&gt;This isn’t a silent background process.&lt;/p&gt;

&lt;p&gt;If notes are being moved, the CLI tells you how many it found, how many migrated, and what got skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let the user choose
&lt;/h3&gt;

&lt;p&gt;Migration only runs if the user decides to.&lt;/p&gt;

&lt;p&gt;The CLI checks once during &lt;code&gt;note setup&lt;/code&gt;, but it’s up to the user whether to run &lt;code&gt;note migrate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No auto-magic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe to run more than once
&lt;/h3&gt;

&lt;p&gt;If the migration already happened, running it again just says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;No legacy notes found. Nothing to migrate.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No side effects, no surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Working on this migration helped me see that data migration is more than just a technical task. It touches multiple areas — technology, product design, and user experience.&lt;/p&gt;

&lt;p&gt;Even for a lightweight CLI tool, data needs to be treated as a valuable user asset. Losing data means losing user trust, and that’s not easy to regain.&lt;/p&gt;

&lt;p&gt;A good migration might run only once — but how it’s designed says a lot about how much you care about your users.&lt;/p&gt;

&lt;p&gt;If this helped you, I write about real dev problems at &lt;a href="https://chloezhou.dev" rel="noopener noreferrer"&gt;chloezhou.dev&lt;/a&gt; — you can subscribe to get new posts by email.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>tooling</category>
      <category>productivity</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Error Handling in CLI Tools: A Practical Pattern That’s Worked for Me</title>
      <dc:creator>Chloe Zhou</dc:creator>
      <pubDate>Thu, 19 Jun 2025 02:00:17 +0000</pubDate>
      <link>https://dev.to/czhoudev/error-handling-in-cli-tools-a-practical-pattern-thats-worked-for-me-2cf0</link>
      <guid>https://dev.to/czhoudev/error-handling-in-cli-tools-a-practical-pattern-thats-worked-for-me-2cf0</guid>
      <description>&lt;p&gt;I’ve been building a small CLI tool recently to help manage personal notes from the terminal. It’s a simple project, but adding features like persistent user sessions and database access made me think more seriously about error handling.&lt;/p&gt;

&lt;p&gt;In particular, I wanted to find a balance between surfacing helpful messages to users while keeping my codebase clean and predictable. This post documents the approach I landed on, why I chose it, and how it plays out in a few real command implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Error Handling Matters in CLI Tools
&lt;/h2&gt;

&lt;p&gt;When designing error handling for a CLI tool, my goal was to make sure that any failure a user runs into is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Human-readable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actionable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context-aware&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get there, I explored common error handling patterns in async JavaScript — specifically how to structure error throwing in utility functions versus catching in command handlers, and how to categorize different types of errors. I ended up with an approach that distinguishes between expected errors, system errors, and business logic errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Common Patterns
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pattern 1: Throw Errors&lt;/strong&gt; (Recommended for CLI)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pattern 2: Return Error Objects&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me show you what they look like in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Throw Errors (Recommended for CLI)
&lt;/h3&gt;

&lt;p&gt;This pattern has low-level functions throw errors when something goes wrong. The errors bubble up to the command handler, which catches them and displays a friendly message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saveUserSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;try&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;ensureUserDir&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;sessionData&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="nx"&gt;user&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="na"&gt;username&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;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loginTime&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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USER_SESSION_PATH&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;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionData&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&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="nx"&gt;sessionData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;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;`Could not save user session: &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;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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command handler then handles all errors in one place:&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;setup &amp;lt;username&amp;gt;&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;Setup user&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;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;argv&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;findOrCreateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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;saveUserSession&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;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="s2"&gt;`✅ Successfully logged in as: &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;username&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&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;span class="nx"&gt;error&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach keeps the command code clean and focused. You only deal with errors once, and you get to present consistent messages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Return Error Objects (Alternative)
&lt;/h3&gt;

&lt;p&gt;Here, low-level functions catch errors themselves and return objects indicating success or failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saveUserSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;try&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;ensureUserDir&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;sessionData&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="nx"&gt;user&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="na"&gt;username&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;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loginTime&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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USER_SESSION_PATH&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;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionData&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionData&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&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;`Could not save user session: &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;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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then every caller must check the returned object explicitly:&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;setup &amp;lt;username&amp;gt;&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;Setup user&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;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userResult&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;findOrCreateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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;userResult&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&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;span class="nx"&gt;userResult&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="k"&gt;return&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;sessionResult&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;saveUserSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userResult&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="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;sessionResult&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&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;span class="nx"&gt;sessionResult&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&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="s2"&gt;`✅ Successfully logged in as: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userResult&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="nx"&gt;username&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;While this pattern makes errors explicit, it can lead to repetitive and verbose code, especially in command handlers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Prefer Pattern 1 (Throw Errors)
&lt;/h3&gt;

&lt;p&gt;This pattern feels like a better fit for CLI tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low-level modules&lt;/strong&gt; throw meaningful errors when things go wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors bubble up&lt;/strong&gt; automatically through the call stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-level command handlers&lt;/strong&gt; catch them once and show user-friendly messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes&lt;/strong&gt; tell the shell that something failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps responsibilities clear: helper functions focus on their job, command handlers focus on user communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling Strategy by Type
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Expected "Errors" (Not Really Errors)
&lt;/h3&gt;

&lt;p&gt;Some conditions aren't really errors — they're just normal edge cases that we expect to happen occasionally. For example, if there's no session file, that simply means the user hasn't logged in yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getUserSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;await&lt;/span&gt; &lt;span class="nf"&gt;access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USER_SESSION_PATH&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;sessionData&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;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USER_SESSION_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&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="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;sessionData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENOENT&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// File doesn't exist = no session (EXPECTED)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Unexpected error&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. System Errors
&lt;/h3&gt;

&lt;p&gt;These usually come from the underlying platform — e.g. Node.js APIs, the file system, or corrupted files. They're rare but should be surfaced with context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saveUserSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;try&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;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USER_SESSION_PATH&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;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionData&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;sessionData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Transform technical error into user-friendly message&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;`Could not save user session: &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;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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Business Logic Errors
&lt;/h3&gt;

&lt;p&gt;These happen when users violate your application's rules or skip required steps. The system works fine, but the user needs to do something differently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requireUserSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;getUserSession&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;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This is a business rule violation&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="s1"&gt;No user session found. Please run "note setup &amp;lt;username&amp;gt;" first.&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="nx"&gt;session&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;h3&gt;
  
  
  The Key Insight
&lt;/h3&gt;

&lt;p&gt;Notice how each type gets handled differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expected conditions&lt;/strong&gt; → Return null or default values, don't throw&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System errors&lt;/strong&gt; → Wrap with context, then throw&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business logic errors&lt;/strong&gt; → Throw with clear instructions for the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach means your command handlers can catch everything with one &lt;code&gt;try/catch&lt;/code&gt;, but users get appropriate messages for each situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Error Flow Example
&lt;/h2&gt;

&lt;p&gt;Let's walk through how the full error handling flow works — from throwing to catching to presenting.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Low-Level: Throw with Context
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clearUserSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;await&lt;/span&gt; &lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USER_SESSION_PATH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENOENT&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// File doesn't exist = mission accomplished anyway&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;`Failed to clear session: &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;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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this level, we care about &lt;em&gt;what&lt;/em&gt; failed, not &lt;em&gt;how&lt;/em&gt; to explain it to the user. We handle the expected case (no file) and throw system errors with context.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Business Logic Layer: Enforce Rules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requireUserSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;getUserSession&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;session&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="s1"&gt;No user session found. Please run "note setup &amp;lt;username&amp;gt;" first.&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="nx"&gt;session&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 enforces a business rule: "You must be logged in to logout." We throw a specific message that tells the user exactly what to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Command Layer: Catch + Present
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logout&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;Clear current user session&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;async &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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;requireUserSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Business rule check&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;clearUserSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Low-level operation  &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="s2"&gt;`✓ Logged out &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; successfully.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&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;span class="nx"&gt;error&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the one place where we actually &lt;em&gt;talk&lt;/em&gt; to the user. We catch everything, show a friendly message, and exit with a non-zero code to signal failure.&lt;/p&gt;

&lt;p&gt;Now it's a true connected flow: check session → clear session → report success, with proper error handling at each layer!&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low-level functions&lt;/strong&gt;: throw meaningful errors with context, handle expected cases gracefully (like &lt;code&gt;ENOENT&lt;/code&gt; → return success)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected cases&lt;/strong&gt;: don't throw for normal situations — return appropriate values instead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business logic violations&lt;/strong&gt;: throw with clear, actionable messages that tell users what to do next&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command handlers&lt;/strong&gt;: catch all errors in one place, present friendly feedback, and call &lt;code&gt;process.exit(1)&lt;/code&gt; for failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error messages&lt;/strong&gt;: be specific and actionable — tell users exactly what went wrong and how to fix it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes&lt;/strong&gt;: use &lt;code&gt;process.exit(1)&lt;/code&gt; so scripts and shells know something failed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Out (And Stay Tuned)
&lt;/h2&gt;

&lt;p&gt;The error handling strategies in this post are part of a broader upgrade I'm working on for my CLI tool &lt;a href="https://www.npmjs.com/package/czhou-notes-cli" rel="noopener noreferrer"&gt;czhou-notes-cli&lt;/a&gt;. The current version stores notes locally and is already usable.&lt;/p&gt;

&lt;p&gt;You can try it now via:&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;-g&lt;/span&gt; czhou-notes-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right now I'm actively improving it — adding things like database support and smoother command experience — and this error handling refactor is just one piece of the puzzle.&lt;/p&gt;

&lt;p&gt;If you give it a try and have any ideas or suggestions, feel free to open an &lt;a href="https://github.com/chloezhoudev/czhou-notes-cli/issues" rel="noopener noreferrer"&gt;issue&lt;/a&gt; or just let me know — I'd love to hear your thoughts!&lt;/p&gt;

&lt;p&gt;Thanks for reading 😊&lt;/p&gt;

&lt;p&gt;If this helped you, I write about real dev problems at &lt;a href="https://chloezhou.dev" rel="noopener noreferrer"&gt;chloezhou.dev&lt;/a&gt; — you can subscribe to get new posts by email.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>node</category>
      <category>javascript</category>
      <category>cli</category>
    </item>
    <item>
      <title>Deploying Express + TypeScript + Prisma to Render (2025): What Went Wrong (and How I Fixed It)</title>
      <dc:creator>Chloe Zhou</dc:creator>
      <pubDate>Wed, 04 Jun 2025 00:07:48 +0000</pubDate>
      <link>https://dev.to/czhoudev/deploying-express-typescript-prisma-to-render-what-went-wrong-and-how-i-fixed-it-5d3l</link>
      <guid>https://dev.to/czhoudev/deploying-express-typescript-prisma-to-render-what-went-wrong-and-how-i-fixed-it-5d3l</guid>
      <description>&lt;p&gt;When I deployed my &lt;strong&gt;Express + TypeScript + Prisma&lt;/strong&gt; backend to Render, I didn’t expect to spend an entire afternoon chasing down one error after another — but that’s exactly what happened. This post is a personal log of all the unexpected problems I hit, what they actually meant, and what I did to get things working again.&lt;/p&gt;

&lt;p&gt;I’m writing this down in case someone else (or future me) runs into the same stack of issues and needs a sanity check.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔴 Error 1: &lt;code&gt;process&lt;/code&gt; and &lt;code&gt;console&lt;/code&gt; not found in TypeScript
&lt;/h3&gt;

&lt;h4&gt;
  
  
  💥 What was the error?
&lt;/h4&gt;

&lt;p&gt;During the build, I saw this in the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot find name ‘process’.
Cannot find name ‘console’.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript didn’t seem to recognize basic Node.js globals. I thought this was weird — these should be available by default, right?&lt;/p&gt;

&lt;h4&gt;
  
  
  🧪 What I tried
&lt;/h4&gt;

&lt;p&gt;I added the following to my &lt;code&gt;tsconfig.json&lt;/code&gt;:&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;"types"&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="s2"&gt;"node"&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="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;…only to be greeted with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot find type definition file for 'node'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had &lt;code&gt;@types/node&lt;/code&gt; installed — but it was under &lt;code&gt;devDependencies&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  ✅ What actually fixed it
&lt;/h4&gt;

&lt;p&gt;Turns out Render doesn’t install &lt;code&gt;devDependencies&lt;/code&gt; during production builds by default. Once I moved &lt;code&gt;@types/node&lt;/code&gt; to &lt;code&gt;dependencies&lt;/code&gt;, the build succeeded.&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; @types/node &lt;span class="nt"&gt;--save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔️ &lt;strong&gt;Lesson learned&lt;/strong&gt;: If your app builds before running (like most TypeScript setups), your build-time tools and types must be in &lt;code&gt;dependencies&lt;/code&gt;, not &lt;code&gt;devDependencies&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔴 Error 2: Cannot find index.js on Render
&lt;/h3&gt;

&lt;h4&gt;
  
  
  💥 What was the error?
&lt;/h4&gt;

&lt;p&gt;Another build, another facepalm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error: Cannot find module '/opt/render/project/src/index.js'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render was trying to start my app using &lt;code&gt;node index.js&lt;/code&gt;, but my compiled code lived in &lt;code&gt;dist/index.js&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  🧪 What I tried
&lt;/h4&gt;

&lt;p&gt;I updated &lt;code&gt;package.json&lt;/code&gt;:&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;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&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;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node dist/index.js"&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;Still didn’t work.&lt;/p&gt;

&lt;h4&gt;
  
  
  ✅ What actually fixed it
&lt;/h4&gt;

&lt;p&gt;Render’s &lt;strong&gt;Start Command&lt;/strong&gt; was still set to &lt;code&gt;node index.js&lt;/code&gt; in the Dashboard.&lt;/p&gt;

&lt;p&gt;Once I changed it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…everything clicked into place.&lt;/p&gt;

&lt;p&gt;✔️ &lt;strong&gt;Lesson learned&lt;/strong&gt;: Your local scripts don’t override what Render runs. Double-check the &lt;strong&gt;Start Command&lt;/strong&gt; and &lt;strong&gt;Build Command&lt;/strong&gt; fields in the Render Dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔴 Error 3: @prisma/client did not initialize
&lt;/h3&gt;

&lt;h4&gt;
  
  
  💥 What was the error?
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@prisma/client did not initialize yet. Please run "prisma generate" and try to import it again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, I wasn’t even surprised anymore.&lt;/p&gt;

&lt;h4&gt;
  
  
  🧪 What I tried
&lt;/h4&gt;

&lt;p&gt;I checked my build script. It was just:&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="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…but Prisma Client needs to be generated &lt;em&gt;before&lt;/em&gt; TypeScript compiles the code that imports it. Oops.&lt;/p&gt;

&lt;p&gt;Also, I noticed my &lt;code&gt;schema.prisma&lt;/code&gt; had a custom output path like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ✅ What actually fixed it
&lt;/h4&gt;

&lt;p&gt;I updated the Prisma generator config to use the default output path (which lives inside &lt;code&gt;node_modules/.prisma/client&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generator client {
  provider = "prisma-client-js"
  output   = "../node_modules/.prisma/client"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I updated the build script to ensure Prisma Client gets generated before compilation:&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="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prisma generate --no-engine &amp;amp;&amp;amp; tsc"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also updated the import to:&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;PrismaClient&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;@prisma/client&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;✔️ &lt;strong&gt;Lesson learned&lt;/strong&gt;: Prisma Client must be generated before compilation. Also, using the default output path avoids a lot of edge cases — especially when deploying.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Final Checklist (So I Don’t Forget Again)
&lt;/h3&gt;

&lt;p&gt;Here’s a checklist of everything I ended up fixing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Move &lt;code&gt;@types/node&lt;/code&gt; from &lt;code&gt;devDependencies&lt;/code&gt; → &lt;code&gt;dependencies&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use start: &lt;code&gt;node dist/index.js&lt;/code&gt; in scripts&lt;/li&gt;
&lt;li&gt;Set Build Command to &lt;code&gt;npm run build&lt;/code&gt; on Render&lt;/li&gt;
&lt;li&gt;Set Start Command to &lt;code&gt;npm start&lt;/code&gt; on Render&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;prisma generate&lt;/code&gt; before &lt;code&gt;tsc&lt;/code&gt; in your build step&lt;/li&gt;
&lt;li&gt;Avoid customizing Prisma output unless you have to — use the default &lt;code&gt;node_modules/.prisma/client&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;--no-engine&lt;/code&gt; with &lt;code&gt;prisma generate&lt;/code&gt; if you’re deploying or using Prisma Accelerate&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  One last thing
&lt;/h3&gt;

&lt;p&gt;If you’re here because your Render build failed with some weird Prisma or TypeScript error — you’re not alone. It’s usually something small, just hiding in plain sight.&lt;/p&gt;

&lt;p&gt;Hope this helps someone else. If you’ve run into other gotchas with this stack, feel free to share — I’d be happy to learn from your debugging too.&lt;/p&gt;




&lt;p&gt;🛠 Tech Stack / Environment&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js: 18.20.5&lt;/li&gt;
&lt;li&gt;Express: 5.1.0&lt;/li&gt;
&lt;li&gt;TypeScript: 5.8.3&lt;/li&gt;
&lt;li&gt;Prisma: 6.6.0&lt;/li&gt;
&lt;li&gt;@prisma/client: 6.6.0&lt;/li&gt;
&lt;li&gt;ts-node: 10.9.2&lt;/li&gt;
&lt;li&gt;Render deployment: May 2025&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this helped you, I write about real dev problems at &lt;a href="https://chloezhou.dev" rel="noopener noreferrer"&gt;chloezhou.dev&lt;/a&gt; — you can subscribe to get new posts by email.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>typescript</category>
      <category>prisma</category>
    </item>
    <item>
      <title>What I Learned from My First Legacy Frontend Project</title>
      <dc:creator>Chloe Zhou</dc:creator>
      <pubDate>Thu, 01 May 2025 07:17:01 +0000</pubDate>
      <link>https://dev.to/czhoudev/what-i-learned-from-my-first-legacy-frontend-project-2h8b</link>
      <guid>https://dev.to/czhoudev/what-i-learned-from-my-first-legacy-frontend-project-2h8b</guid>
      <description>&lt;p&gt;This was my first time taking over a legacy frontend codebase - one that had been around for over five years.&lt;/p&gt;

&lt;p&gt;The system was built with React 16.8 and relied heavily on class components. This code was tightly coupled, hard to follow, and even harder to extend. Some components had over thousand lines, with tangled logic and strong dependencies. If you change one thing, you couldn't be sure what else might break.&lt;/p&gt;

&lt;p&gt;My task sounded straightforward: support a new business type with some custom fields and a different layout. But as I dug in, I realized it was more than just adding a few inputs. I had to reuse existing logic, introduce new behaviors, and make sure I didn't break anything already in use. &lt;strong&gt;It wasn't just development - it was kind of software surgery.&lt;/strong&gt; Every change had to be precise. One wrong move, and things could break in ways you wouldn't expect.&lt;/p&gt;

&lt;p&gt;This article doesn't dive into technical implementation, nor is it meant to complain about how messy legacy code can be. Instead, I want to document the three main pitfalls I encountered during my first legacy delivery - and the three key lessons I learned from them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pit #1: Don't Refactor Logic Without Fully Understanding the System First&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before I started the actual delivery of the new feature, I had a bit of extra time, so I decided to take the opportunity to familiarize myself with the module that I was about to modify. As I dug into the code, I noticed that some parts of the logic had very tight coupling. This made me think that it might be worth refactoring those sections as part of the preparation for the upcoming delivery. It seemed like a good idea at the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But I made a crucial mistake. I didn't have a complete understanding of the logic I was about to refactor.&lt;/strong&gt; I thought I had a good grasp of it, but in reality, I didn't know the ins and outs of the code well enough. I didn't fully comprehend how it interacted with other parts of the system, what its true dependencies were, or how the inputs and outputs were structured.&lt;/p&gt;

&lt;p&gt;Here's the key lesson: &lt;strong&gt;when you're refactoring code, you need to have 100% clarity on the logic you're working with.&lt;/strong&gt; That doesn't mean understanding the entire system, but understanding &lt;strong&gt;your specific module&lt;/strong&gt; - what dependencies it has, and what its inputs and outputs are - is essential.&lt;/p&gt;

&lt;p&gt;Some of you might be thinking, "Why not just write tests first, and then refactor? Isn't that what TDD is for?" &lt;strong&gt;Well, yes… and no.&lt;/strong&gt; While TDD is a widely recommended approach, and it could certainly help in a clean, modern codebase, the reality is quite different when you're dealing with a legacy system like the one I was working with. And let's be honest - who really enjoys writing tests?&lt;/p&gt;

&lt;p&gt;After I refactored the code, I found that the behavior had changed unexpectedly. This made it difficult for me to trace back the original logic and behavior, as I had unintentionally altered it without fully understanding it in the first place.&lt;/p&gt;

&lt;p&gt;To fix this, I needed to identify where the issue came from. I ran tests in my UAT environment, where I could compare the new behavior with the original. By doing this, I was able to debug the inconsistencies between what was happening in my local development environment and the behavior in the higher environment. This process helped me pinpoint where my logic change had led to unexpected outcomes, and I was able to correct the mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So the core lesson here:&lt;/strong&gt; never blindly refactor code without fully understanding the business context and logic behind it. Even if it seems like a "pure UI improvement", it can have unintended consequences that affect the overall system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pit #2: Don't Wait Until You Fully Understand the System to Start Refactoring - Embrace the "Incremental Approach"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While it's essential to have a solid understanding of the logic you're working with before making changes (as discussed in the first pit), &lt;strong&gt;there's a fine line between being cautious and getting stuck in analysis paralysis.&lt;/strong&gt; The key lesson here is: don't wait to understand every single detail of the system before taking action. Instead, aim for an incremental approach - make small, controlled changes and expand your understanding as you go.&lt;/p&gt;

&lt;p&gt;The reality is that achieving a comprehensive understanding of the entire system is nearly impossible, especially when working with complex legacy code. The system is intricate, with too many moving parts, and expecting to grasp it all upfront can lead to endless analysis with little actual progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So, what's the solution?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find a reasonable entry point where you can start making small, manageable changes&lt;/strong&gt; - this will help build your confidence and gradually expand your understanding. For example, when approaching frontend tasks, it's common practice to start with static UI. Static UI tends to have simpler, more isolated logic, making it a good entry point for understanding the structure of the code.&lt;/p&gt;

&lt;p&gt;Take a look at how the static UI is built. Focus on things like the &lt;code&gt;render&lt;/code&gt; function in your React components, which handles the visual part of the UI. This part is generally less complicated, so it gives you a solid foundation to start making small adjustments. Once you're comfortable with that, you can move on to more interactive parts of the code, tackling them one step at a time.&lt;/p&gt;

&lt;p&gt;This incremental approach works especially well with legacy systems. Trying to understand everything upfront before making any changes can slow you down unnecessarily. Instead, by taking it step-by-step, you can start making progress right away and build your understanding as you go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pit #3: Migrating Lifecycle Methods Isn't Just About Replacing Them&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I took over this legacy code, I was already accustomed to React 18 and functional components with hooks, so I wasn't very familiar with class components and their lifecycle methods - especially the ones like &lt;code&gt;componentWillReceiveProps&lt;/code&gt;, which is deprecated, and &lt;code&gt;getDerivedStateFromProps&lt;/code&gt;, which is the recommended alternative. My initial instinct was to replace these deprecated methods with their newer counterparts. But soon, I realized that migration isn't just about swapping one lifecycle method for another.&lt;/p&gt;

&lt;p&gt;React provides alternatives, such as &lt;code&gt;getDerivedStateFromProps&lt;/code&gt; for &lt;code&gt;componentWillReceiveProps&lt;/code&gt;. However, migrating lifecycle methods isn't as simple as finding a "replacement." Each lifecycle method is triggered at specific points in the component's lifecycle and serves particular use cases. Understanding how and when each method is invoked is critical. Without this understanding, you risk introducing new issues or inconsistencies.&lt;/p&gt;

&lt;p&gt;Take my specific case, for example. In the project I was working on, &lt;code&gt;componentWillReceiveProps&lt;/code&gt; was in use, and my goal was to migrate it to &lt;code&gt;getDerivedStateFromProp&lt;/code&gt;s. The reason for this migration was that the new requirement was to update the component's state based on new props, which made &lt;code&gt;getDerivedStateFromProps&lt;/code&gt; a suitable alternative. However, there's a &lt;strong&gt;catch&lt;/strong&gt; - &lt;code&gt;componentWillReceiveProps&lt;/code&gt; and &lt;code&gt;getDerivedStateFromProps&lt;/code&gt; have different behaviors and usage patterns.&lt;/p&gt;

&lt;p&gt;After analyzing the existing code, I realized that &lt;code&gt;componentWillReceiveProps&lt;/code&gt; was already being used to update the state based on new props, but it was updating different properties of the state. Given that the new requirements aligned with what &lt;code&gt;getDerivedStateFromProps&lt;/code&gt; is intended for - updating state based on new props - I saw an &lt;strong&gt;opportunity&lt;/strong&gt; to merge the new requirements with the existing logic. By doing this, I was able to combine the old logic with the new behavior, making the migration smoother.&lt;/p&gt;

&lt;p&gt;Lesson learned: Migrating lifecycle methods isn't just about replacing deprecated methods with the new ones React provides. It's about understanding the logic behind each method, when it's triggered, and whether it fits your specific needs. In my case, I needed to ensure that the new and old logic could be combined effectively before proceeding with the migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respect the System, Respect the Unknown&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Legacy systems aren't monsters. They've grown over time to support real, often messy business needs. The complexity you see is rarely accidental - it reflects years of decisions, constraints, and trade-offs.&lt;/p&gt;

&lt;p&gt;After delivering my first feature on this project, I started to see things more clearly. Respecting the system doesn't mean staying hands-off - it just means knowing what's already there, and being cautious about what might break when you change it. &lt;strong&gt;You can still refactor boldly, but only if you understand the impact.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When things feel overwhelming, it helps to find one small, solid starting point. Something you're sure about. From there, piece by piece, you build up enough context to move forward. That's how I've learned to work with legacy code - not by fully mastering it first, but by finding my footing one step at a time.&lt;/p&gt;

&lt;p&gt;What about you? Have you worked on legacy projects? Let me know below!&lt;/p&gt;

&lt;p&gt;If this helped you, I write about real dev problems at &lt;a href="https://chloezhou.dev" rel="noopener noreferrer"&gt;chloezhou.dev&lt;/a&gt; — you can subscribe to get new posts by email.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>legacycode</category>
    </item>
    <item>
      <title>How I Built Schedulicious: A Meal Planning Web App</title>
      <dc:creator>Chloe Zhou</dc:creator>
      <pubDate>Mon, 17 Feb 2025 07:39:20 +0000</pubDate>
      <link>https://dev.to/czhoudev/how-i-built-schedulicious-a-meal-planning-web-app-576a</link>
      <guid>https://dev.to/czhoudev/how-i-built-schedulicious-a-meal-planning-web-app-576a</guid>
      <description>&lt;h3&gt;
  
  
  How it Started
&lt;/h3&gt;

&lt;p&gt;I joined &lt;a href="https://www.chingu.io/" rel="noopener noreferrer"&gt;Chigu&lt;/a&gt; during my job search because I wanted to stay productive, sharpen my skills, and gain more hands-on experience working in a team. &lt;/p&gt;

&lt;p&gt;Job hunting can be a unpredictable process, and instead of waiting around, I saw Chigu as the perfect opportunity to build something meaningful while collaborating with others. &lt;/p&gt;

&lt;p&gt;Looking back, it turned out to be one of the best decisions I made—not only did I improve my technical and teamwork skills, but I also got to experience the full process of bringing a project from 0 to 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Built
&lt;/h3&gt;

&lt;p&gt;The project is a &lt;strong&gt;meal planning tool&lt;/strong&gt; designed to help managers efficiently create weekly menus for employees. &lt;/p&gt;

&lt;h4&gt;
  
  
  Key Features:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-Click Menu Generation&lt;/strong&gt; – Instead of manually selecting meals, managers can generate a balanced menu with a single click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Repeated Meals&lt;/strong&gt; – The system ensures that each day’s dish is unique, preventing repetition throughout the week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy Regeneration&lt;/strong&gt; – If the results aren’t satisfactory, a “regenerate” button allows them to create a new menu instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export Functionality&lt;/strong&gt; – Users can save and track meal plans in PDF or Excel format for easy reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  From 0 to 1
&lt;/h3&gt;

&lt;p&gt;Each team receives a product spec with predefined features, but it’s up to us to break them down, plan the implementation, and bring the product to life. &lt;/p&gt;

&lt;h4&gt;
  
  
  Leading the Project
&lt;/h4&gt;

&lt;p&gt;Our team was unique—no project manager, product owner, or UI/UX designer—just developers. Seeing the opportunity to lead in the absence of a project manager, I volunteered to take on the role. It felt like a perfect challenge to organize the team and drive the project forward.&lt;/p&gt;

&lt;p&gt;I kicked off the project by planning the initial meeting: setting an agenda, defining each team member’s tasks, and ensuring we were aligned on the outcomes. Thanks to this preparation, the meeting was productive, and we defined our MVP features and roles for the upcoming sprint.&lt;/p&gt;

&lt;h4&gt;
  
  
  Building the Product Backlog
&lt;/h4&gt;

&lt;p&gt;In sprint 2, I took on the responsibility of creating a Product Backlog, which is usually handled by a Product Owner. I broke down the MVP features into epics, user stories, and tasks, creating templates to ensure everyone was aligned. &lt;/p&gt;

&lt;p&gt;This process made me realize how essential the backlog is as a roadmap for the team. It wasn’t easy—understanding each feature, defining clear acceptance criteria, and avoiding repetition was a challenge. But as I worked through it, I gained a clearer understanding of how each feature impacts the end user. &lt;/p&gt;

&lt;p&gt;One instance where I exercised product thinking was when I adjusted the feature flow based on my own understanding of user needs and how they would interact with the product to ensure a smooth experience.&lt;/p&gt;

&lt;p&gt;Unlike in more structured environments where backlogs are typically fixed, I had the freedom to adapt the “HOW” during development, tailoring the features to better suit user needs. This process was both challenging and rewarding, sharpening my product thinking and development skills.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Tech Stack
&lt;/h3&gt;

&lt;p&gt;For this project, we didn’t have to worry about a backend since we were working solely with a dish API. Therefore, I chose &lt;strong&gt;React&lt;/strong&gt; as the core of the tech stack, paired with &lt;strong&gt;Vite&lt;/strong&gt; as the build tool to ensure fast development and smooth hot reloading. &lt;/p&gt;

&lt;p&gt;For state management, I initially considered &lt;strong&gt;Context API&lt;/strong&gt; and &lt;strong&gt;Zustand&lt;/strong&gt;, ultimately choosing &lt;strong&gt;Zustand&lt;/strong&gt; for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our use case was simple but involved sharing state across multiple routes (e.g., allergies and weekly menus).&lt;/li&gt;
&lt;li&gt;Zustand provides built-in middleware for local storage persistence, which saved us time and effort in implementing our own solution.&lt;/li&gt;
&lt;li&gt;Out of the box, Zustand offers better performance, as it updates only the relevant parts of the state without unnecessary re-renders.&lt;/li&gt;
&lt;li&gt;Zustand is easier to scale. Should we need to manage more complex states, such as loading or error handling, or handle more sophisticated logic in the future, it can grow with the project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  UI/UX Design
&lt;/h3&gt;

&lt;p&gt;For this MVP, I aimed to create a clean, modern, and easy-to-navigate UI that would allow users to start using the tool immediately without the need for a sign-in or sign-up process. The goal was to minimize friction and make the user experience as seamless as possible. &lt;/p&gt;

&lt;p&gt;A bright color palette, chosen for its ability to convey a sense of openness and clarity, sets the tone for the UI, thanks to our talented UI/UX designer. She laid the foundation for the design, and I tailored the UX to suit the users’ needs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Designing the Swap Feature
&lt;/h4&gt;

&lt;p&gt;One of the key features I focused on was ensuring users could easily swap dishes if they weren’t happy with their selection. &lt;/p&gt;

&lt;p&gt;Initially, I considered allowing users to search through a large list of dishes, which could be intuitive but might not be the most efficient way to save their time. Instead, I designed a modal popup that presents five recommended replacement dishes. &lt;/p&gt;

&lt;p&gt;These dishes are randomly generated from the available dish database, ensuring no repeats with the current weekly menu. Offering five options strikes a balance between providing enough variety and avoiding overwhelming the user. I believe this design choice helps users make quick, informed decisions without limiting their options. &lt;/p&gt;

&lt;p&gt;Here’s the UI in action:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia4.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExbHkzY3h0Z3o0eDl1eG92MDAwMzRvcmtycHc0eGc5eTVidmtkbjVtaSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FVJlquVE33e0io1POhP%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img width="480" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia4.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExbHkzY3h0Z3o0eDl1eG92MDAwMzRvcmtycHc0eGc5eTVidmtkbjVtaSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FVJlquVE33e0io1POhP%2Fgiphy.gif" height="230"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Overcoming Challenges and Delivering the Project
&lt;/h3&gt;

&lt;p&gt;Just two sprints into the project, our team faced some challenges when two members became less active, and our UI/UX designer, who was also expected to contribute to development, had to step back. &lt;/p&gt;

&lt;p&gt;By the time we were ready to dive into the development phase, it was just me. Although I briefly considered quitting, I decided to push forward and deliver the project on my own. &lt;/p&gt;

&lt;p&gt;I focused on core feature development and refining the user experience. Despite the challenges, I was able to successfully complete the product!&lt;/p&gt;

&lt;p&gt;I shared my full journey on Twitter.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1890735259190440170-769" src="https://platform.twitter.com/embed/Tweet.html?id=1890735259190440170"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1890735259190440170-769');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1890735259190440170&amp;amp;theme=dark"
  }



 &lt;/p&gt;

&lt;p&gt;Special thanks to @numulaa for laying the foundation for the design, and you can find her work here on &lt;a href="https://github.com/numulaa" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Check out the project!
&lt;/h3&gt;

&lt;p&gt;I’m excited to share that this project is open source! You can explore the code, contribute, or just check it out on &lt;a href="https://github.com/chloezhoudev/schedulicious" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you found it useful or interesting, feel free to give it a star ⭐—it would mean a lot!&lt;/p&gt;

&lt;p&gt;Try the live &lt;a href="https://schedulicious.vercel.app/" rel="noopener noreferrer"&gt;demo&lt;/a&gt; or watch the YouTube walkthrough to see it in action! &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/M74mX-gAlKo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;💬 Got feedback? I’d love to hear your thoughts! Feel free to open an issue on GitHub or drop me a message. &lt;/p&gt;

&lt;p&gt;I’ll continue improving and iterating on it—stay tuned for updates! 🚀&lt;/p&gt;

&lt;p&gt;If this helped you, I write about real dev problems at &lt;a href="https://chloezhou.dev" rel="noopener noreferrer"&gt;chloezhou.dev&lt;/a&gt; — you can subscribe to get new posts by email.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
