<?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: Michael Kutateladze</title>
    <description>The latest articles on DEV Community by Michael Kutateladze (@michael_kutateladze_7b8db).</description>
    <link>https://dev.to/michael_kutateladze_7b8db</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2469811%2Fa80f720b-2c42-42db-9c3c-54e9a1339df6.jpg</url>
      <title>DEV Community: Michael Kutateladze</title>
      <link>https://dev.to/michael_kutateladze_7b8db</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/michael_kutateladze_7b8db"/>
    <language>en</language>
    <item>
      <title>Build vs Buy a CSV Importer in 2026: Is It Worth It?</title>
      <dc:creator>Michael Kutateladze</dc:creator>
      <pubDate>Sat, 19 Sep 2026 14:05:57 +0000</pubDate>
      <link>https://dev.to/michael_kutateladze_7b8db/build-vs-buy-a-csv-importer-in-2026-is-it-worth-it-44no</link>
      <guid>https://dev.to/michael_kutateladze_7b8db/build-vs-buy-a-csv-importer-in-2026-is-it-worth-it-44no</guid>
      <description>&lt;p&gt;I have spent the last nine months building a CSV data importer. Before that I worked in the same industry, and I watched import screens get built, shipped and patched. Embedded importers are an old idea, and I have read more than once that the category is dying. Then I ask engineers how they bring customer files into their product, and in 2026 I hear the answer I heard years ago. They are building their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple importer takes a couple of days
&lt;/h2&gt;

&lt;p&gt;The reasoning is the same every time. We know which files our customers have. We have a fixed schema. With an AI assistant, a file input, a parser and a mapping screen take a couple of days.&lt;/p&gt;

&lt;p&gt;All of that is true. For a small company with a few customers it is also the right call. You know the people who send the files, you have seen the files, and when one breaks you fix it quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Growth changes the files
&lt;/h2&gt;

&lt;p&gt;The trouble starts when the product grows. New customers bring files you have never seen, and nobody tells you before they upload.&lt;/p&gt;

&lt;p&gt;One customer arrives with a dozen files at once. Another needs to load a single CSV with 500,000 rows. One file has rows that came out a value short, so everything after the gap sits a column to the left. One has no header row at all. One has a title and two blank lines above the headers. Dates come as 3/4 in one export and 4/3 in the next, and numbers switch between a dot and a comma for the decimal.&lt;/p&gt;

&lt;p&gt;Each of these is a ticket. Each fix is small. The tickets never end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation grows with the customers
&lt;/h2&gt;

&lt;p&gt;The schema was fixed on day one. The rules around it keep moving.&lt;/p&gt;

&lt;p&gt;One customer needs a custom check on an account number. Another needs a check that calls your API, because only the server knows whether that SKU exists. Now validation is asynchronous, it has to batch its requests, and it has to stay out of the way while the person keeps editing.&lt;/p&gt;

&lt;p&gt;Then come the values every product shares. A country can be written as &lt;code&gt;Germany&lt;/code&gt;, &lt;code&gt;Deutschland&lt;/code&gt;, &lt;code&gt;DE&lt;/code&gt; or &lt;code&gt;DEU&lt;/code&gt;, and your database wants one ISO 3166-1 code. A phone number comes with spaces, dots and brackets, and you want to store E.164. Dates want one format on the way out, whatever they looked like on the way in. There are standards for all of this, and somebody on the team has to read them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser has to keep up
&lt;/h2&gt;

&lt;p&gt;After the data questions come the experience questions. The person who uploaded 500,000 rows expects the page to keep scrolling. They expect to fix a bad cell in place, the way they would in Excel, without downloading the file and uploading it again. They expect filters that show only the rows with errors, undo when a fix goes wrong, and copy and paste that behaves the way it does in a spreadsheet.&lt;/p&gt;

&lt;p&gt;A large file stalls a browser in more than one place. Parsing on the main thread freezes the interface. The parsed rows take several times the file size in memory. Rendering them in DOM nodes ends the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full version takes months
&lt;/h2&gt;

&lt;p&gt;You can build all of this yourself. Nobody doubts that. A team can ship an importer for its own happy path in three weeks. The road from that importer to one that handles files from customers you have never seen before is an open-ended project, because the list of files is open-ended. It takes months, and it takes months with AI too. AI writes the first version in days. It does not collect the broken files for you, and it does not decide how a grid should behave with a million rows in it. I know the number because I have been paying it for nine months, full time, on this one problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building is still right for some teams
&lt;/h2&gt;

&lt;p&gt;If one known partner sends one known file shape, stay with your two day importer. It fits the job, and nothing in this post applies to you yet.&lt;/p&gt;

&lt;p&gt;If your import is a scheduled pipeline with SFTP drops and recurring feeds, that is server work, and a browser importer is the wrong shape for it.&lt;/p&gt;

&lt;p&gt;If import is the product your customers pay for, build it and own every line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the months cost
&lt;/h2&gt;

&lt;p&gt;Take $60 an hour as a low estimate for an engineer. A month of focused work is 160 hours, so an engineer month costs $9,600, plus $100 for the AI subscription. Call it $9,700.&lt;/p&gt;

&lt;p&gt;Be optimistic and say the full version takes two months. That is $19,400 spent before the first customer uploads a file. After launch the tickets start, and your team is the one fixing them. The time they spend on the importer is time they do not spend on your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  What buying costs
&lt;/h2&gt;

&lt;p&gt;The market has changed. Importers are turning into a commodity, and a good one no longer needs a sales call.&lt;/p&gt;

&lt;p&gt;I sell one, so read the next number knowing that. &lt;a href="https://updog.tech" rel="noopener noreferrer"&gt;Updog&lt;/a&gt; costs $19 a month per production domain and is free in development. A product serving ten customers, each on its own domain, pays $190 a month.&lt;/p&gt;

&lt;p&gt;For those ten domains, one engineer month pays for more than four years of the subscription, and two months pay for more than eight. Drop the rate to $40 an hour and one month still pays for almost three years.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision belongs in a budget
&lt;/h2&gt;

&lt;p&gt;For everyone outside the cases above this is a business decision, and it needs no heroics. "I am an engineer, I will build it myself" is a true sentence. The same engineer costs $9,700 a month.&lt;/p&gt;

&lt;p&gt;Updog is built around the problems that come with importing customer files into your product and starts at $19 a month. It deals with messy files, runs your validation, and keeps the browser fast on large imports.&lt;/p&gt;

&lt;p&gt;So before you start building, ask yourself whether you are ready to spend two months and thousands of dollars on your own importer, or whether you would rather pay from $19 a month for a maintained product with support when you need it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: The ideas, experience and conclusions in this article are my own. AI was used to help edit, structure and refine the writing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>startup</category>
      <category>saas</category>
    </item>
    <item>
      <title>The First CSV Importer You Write Breaks on Real Files</title>
      <dc:creator>Michael Kutateladze</dc:creator>
      <pubDate>Thu, 17 Sep 2026 15:02:53 +0000</pubDate>
      <link>https://dev.to/michael_kutateladze_7b8db/the-first-csv-importer-you-write-breaks-on-real-files-1bc7</link>
      <guid>https://dev.to/michael_kutateladze_7b8db/the-first-csv-importer-you-write-breaks-on-real-files-1bc7</guid>
      <description>&lt;p&gt;I spent the past nine months building a data importer. It is the screen where a person brings records they already have into a web app, such as a customer list or a price list exported from another system, as a CSV or Excel file. The app reads the file and turns every line into a row it can store.&lt;/p&gt;

&lt;p&gt;Reading a CSV looks like a small job, and a first version can fit in six lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Papa&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;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;header&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;dynamicTyping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;setRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a reasonable first CSV import. PapaParse reads the text, &lt;code&gt;header: true&lt;/code&gt; turns every line into an object keyed by column name, and &lt;code&gt;dynamicTyping: true&lt;/code&gt; turns numeric strings into numbers. A test file goes through it cleanly, because the developer wrote that file to fit the code.&lt;/p&gt;

&lt;p&gt;When the file comes from a system you control and already matches the shape your app expects, this code can be enough. A file from someone else follows the conventions of the software that produced it, such as how it writes dates, decimals, and IDs, and each of those conventions can break this code in its own way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type inference strips leading zeros
&lt;/h2&gt;

&lt;p&gt;Some values look like numbers and are not. Customer IDs, ZIP codes, and product codes are made of digits, and the zeros at the front belong to the value. A parser that converts everything numeric into a number throws those zeros away.&lt;/p&gt;

&lt;p&gt;A customer export holds a &lt;code&gt;customer_id&lt;/code&gt; column with the values &lt;code&gt;00123&lt;/code&gt;, &lt;code&gt;00401&lt;/code&gt;, and &lt;code&gt;07030&lt;/code&gt;. &lt;code&gt;dynamicTyping&lt;/code&gt; tests each value against a number pattern, and on PapaParse 5.6.0 that pattern accepts leading zeros, so &lt;code&gt;"00123"&lt;/code&gt; becomes &lt;code&gt;123&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The row still looks valid. It stops matching customer &lt;code&gt;00123&lt;/code&gt; the moment the app uses the value as an identifier.&lt;/p&gt;

&lt;p&gt;The import keeps the raw cell text and lets the schema decide the type. A field declared as an ID stays text. A field declared as a number gets checked as a number, and &lt;code&gt;00123&lt;/code&gt; in that field becomes a value to flag. The parser sees one value at a time and has no way to tell an ID from an amount. The schema knows which field is which.&lt;/p&gt;

&lt;p&gt;In an Excel file, a cell formatted as &lt;code&gt;00000&lt;/code&gt; stores &lt;code&gt;401&lt;/code&gt; and shows &lt;code&gt;00401&lt;/code&gt;, so the importer reads the formatted text.&lt;/p&gt;

&lt;p&gt;Some zeros are gone before the file reaches you. Microsoft documents that Excel "automatically removes leading zeros", so a CSV that passed through Excel can arrive holding &lt;code&gt;401&lt;/code&gt;. Nothing in that value says the ID had five digits. Your own code pads it back, because only your app knows the width.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole column reveals the date format
&lt;/h2&gt;

&lt;p&gt;A date written as 03/04/2025 means 3 April in the UK and 4 March in the US. The file does not say which convention it follows, and JavaScript picks one on its own.&lt;/p&gt;

&lt;p&gt;An export from a UK system writes dates day first. The obvious conversion hands each value to the &lt;code&gt;Date&lt;/code&gt; constructor.&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;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;03/04/2025&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Tue Mar 04 2025&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;28/11/2025&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Invalid Date&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15/01/2026&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Invalid Date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In V8, the engine behind Chrome and Node, the first value parses without an error and lands on 4 March. The file meant 3 April. The other two fail, so one column produces a silent wrong date and two errors.&lt;/p&gt;

&lt;p&gt;One value cannot settle the format, because &lt;code&gt;03/04/2025&lt;/code&gt; is a valid date read either way. A sample from the whole column can. The importer takes a sample of values from the column and lets every candidate format try to read it, day first, month first, year first, and the rest. The format that reads the most values wins. &lt;code&gt;28/11/2025&lt;/code&gt; has no 28th month, so month first fails on it and loses the count. Day first reads all three, wins the column, and &lt;code&gt;03/04/2025&lt;/code&gt; becomes 3 April for every row.&lt;/p&gt;

&lt;p&gt;A column holding only &lt;code&gt;03/04/2025&lt;/code&gt; and &lt;code&gt;05/06/2025&lt;/code&gt; reads fully both ways, and the data cannot break that tie. A convention from outside the data has to, such as the locale of the browser. The importer should report that it guessed, because a wrong guess produces dates that look valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole column reveals the separators
&lt;/h2&gt;

&lt;p&gt;Countries write numbers differently. One thousand two hundred thirty-four and fifty-six hundredths is 1,234.56 in the US and 1.234,56 in Germany. The dot and the comma swap roles, and code written for one convention misreads the other.&lt;/p&gt;

&lt;p&gt;Price columns from different systems hold &lt;code&gt;1.234,56&lt;/code&gt;, &lt;code&gt;1 234,56&lt;/code&gt; with a non-breaking space, &lt;code&gt;€1,234.56&lt;/code&gt;, and &lt;code&gt;(250,00)&lt;/code&gt;, which accounting software uses for a negative amount. &lt;code&gt;parseFloat&lt;/code&gt; reads each of them from the left and stops at the first character it does not expect.&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;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.234,56&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 1.234&lt;/span&gt;
&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1 234,56&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;€1,234.56&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// NaN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A validator catches &lt;code&gt;NaN&lt;/code&gt;. &lt;code&gt;1.234&lt;/code&gt; passes the same check, because it is a valid number, and it is a thousand times smaller than the amount in the file.&lt;/p&gt;

&lt;p&gt;The column votes again, and this time the candidates are pairs of marks, one that groups thousands and one that starts the decimals. Dot and comma is one pair, comma and dot another, space and comma a third. A pair reads a value only when the digit groups sit where that pair expects them, so &lt;code&gt;1.234,56&lt;/code&gt; fits dot and comma and fails comma and dot. Before the vote, the importer strips the currency symbol, normalizes the non-breaking space, and turns &lt;code&gt;(250,00)&lt;/code&gt; into &lt;code&gt;-250&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The column settles values that look identical on their own. &lt;code&gt;1,234&lt;/code&gt; means 1234 in a column that also holds &lt;code&gt;1,234.56&lt;/code&gt;, and 1.234 in a column that also holds &lt;code&gt;1.234,56&lt;/code&gt;. A column of nothing but &lt;code&gt;1,234&lt;/code&gt; ties the way dates do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Value mapping works on distinct values
&lt;/h2&gt;

&lt;p&gt;Some fields accept only a fixed list of values, such as a department, a status, or a country. People type those values in their own words, with different spelling, case, and abbreviations. The importer turns every variant into one of the values the app accepts.&lt;/p&gt;

&lt;p&gt;The file's &lt;code&gt;Department&lt;/code&gt; column already matched the &lt;code&gt;department&lt;/code&gt; field by its header, and a lookup table converts the words.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEPARTMENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Engineering&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eng&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Sales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sales&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DEPARTMENTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column holds &lt;code&gt;Engineering&lt;/code&gt;, &lt;code&gt;engineering&lt;/code&gt;, &lt;code&gt;Eng&lt;/code&gt;, &lt;code&gt;R&amp;amp;D&lt;/code&gt;, and &lt;code&gt;Enginering&lt;/code&gt;. The table finds the first and returns &lt;code&gt;undefined&lt;/code&gt; for the other four. The header matched, and the values inside the column still use the words of whoever typed them.&lt;/p&gt;

&lt;p&gt;The importer collects the unique values of the column, resolves each one once, and applies the finished mapping to every row as a lookup. A million rows with five spellings of a department need five decisions. Fuzzy string matching that ignores case pairs &lt;code&gt;engineering&lt;/code&gt; and &lt;code&gt;Enginering&lt;/code&gt; with &lt;code&gt;Engineering&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;String similarity alone does not tell you that &lt;code&gt;R&amp;amp;D&lt;/code&gt; means &lt;code&gt;Engineering&lt;/code&gt;, or that &lt;code&gt;Eng&lt;/code&gt; is short for it. That knowledge belongs to your domain. It reaches the mapping through a synonym list you keep, through your own logic, such as the mappings the same customer confirmed last time, or through the person importing the file, who picks the value by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser decodes every file as UTF-8
&lt;/h2&gt;

&lt;p&gt;A file on disk is a sequence of bytes, and an encoding is the rule that turns those bytes into letters. Older systems and different countries use different rules. When the app reads a file with the wrong rule, letters such as ü and é come out broken.&lt;/p&gt;

&lt;p&gt;The import code at the start of this article reads the file with &lt;code&gt;file.text()&lt;/code&gt;, which always decodes it as UTF-8. A CSV written by an older system in Windows-1252 stores &lt;code&gt;ü&lt;/code&gt; as the single byte &lt;code&gt;FC&lt;/code&gt;, which is invalid in UTF-8. The decoder swaps it for a replacement character, and &lt;code&gt;Müller&lt;/code&gt; arrives as &lt;code&gt;M�ller&lt;/code&gt;. The opposite mistake, UTF-8 bytes read as Windows-1252, turns &lt;code&gt;Müller&lt;/code&gt; into &lt;code&gt;MÃ¼ller&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The replacement character is final. Once &lt;code&gt;�&lt;/code&gt; sits in the string, nothing recovers the letter it replaced, so the decision has to happen while the bytes are still in hand. The importer checks for a byte order mark first, because it can identify several Unicode encodings before any guessing is needed. Then it tries UTF-8 in strict mode, where an invalid byte makes the decoder throw.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fatal&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If strict decoding throws, the bytes are not valid UTF-8. Only then does the importer infer a legacy encoding from the bytes with a detector such as chardet, and decode again with the label it returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Large files break both parsing and rendering
&lt;/h2&gt;

&lt;p&gt;Every fix so far works on a file of a few hundred rows. A file with a million rows asks the browser to read, process, and show all of it while the page stays responsive.&lt;/p&gt;

&lt;p&gt;The import code at the start of this article runs on the main thread, which also paints the page and answers clicks. As the importer gains decoding, parsing, format detection, and value mapping, doing that work on the main thread turns it into one long task, and the tab stops responding until it ends.&lt;/p&gt;

&lt;p&gt;The importer hands the &lt;code&gt;File&lt;/code&gt; object to a Web Worker and decodes and parses there, so the page keeps painting. Some cost stays on the main thread. On 4 September 2026 I ran a 108 MB CSV with one million rows and eleven columns through a worker in Chrome 151. The main thread stayed free while the worker parsed, and still hit one 389 ms task when the parsed rows arrived back. A worker that reads the file in one call also holds the whole file in memory.&lt;/p&gt;

&lt;p&gt;The rows then reach the second limit. A normal DOM table does not scale to a million rows, because every cell it renders is an element the browser lays out and paints. The grid has to limit rendering to a small window around the visible rows and work out the rest from the scroll position. A virtualized list does that with elements, and a canvas grid does it with pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  An importer answers what the data means
&lt;/h2&gt;

&lt;p&gt;Other files break a simple CSV import in other ways. A report can place its header row several lines down, under a title and a blank line. Two columns can share the header &lt;code&gt;id&lt;/code&gt;, and PapaParse renames the second to &lt;code&gt;id_1&lt;/code&gt;, which no field in the app expects. A short row can shift its values into the wrong columns. An Excel workbook can hold several sheets, and only one of them holds the data.&lt;/p&gt;

&lt;p&gt;A CSV parser answers whether a file can be read. An importer has to answer what the data means, what is wrong with it, what the person can fix, and what the app should receive.&lt;/p&gt;

&lt;p&gt;I learned most of this while building &lt;a href="https://updog.tech" rel="noopener noreferrer"&gt;Updog&lt;/a&gt;, a client-side importer for CSV and Excel files.&lt;/p&gt;

&lt;p&gt;I wrote this article with the help of AI.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>csv</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
