β€” Β· 5 min read

Convex post-mortem: the TypeScript error that came from nowhere

Convex logo

I was building a POC for a credit-based SaaS on Next.js + Convex for a customer. Users subscribe to a monthly plan through Stripe, Stripe sends webhooks to Convex, Convex credits their balance. Classic stuff.

The Stripe webhook handlers run through Convex actions with "use node" because the Stripe SDK needs Node.js. Fine.

Right after I shipped that, the Next.js build broke.

The symptom

The error was on a completely innocent admin page. A list. A .map(). Nothing:

const rows = useQuery(api.credits.adminListBalances);
 
return (
  <ul>
    {rows.map((row) => (
      <li key={row._id}>
        {row.email} - {row.balance}
      </li>
    ))}
  </ul>
);
./app/app/admin/credits/page.tsx:19:22
Type error: Parameter 'row' implicitly has an 'any' type.

  17 |       ) : (
  18 |         <ul>
> 19 |           {rows.map((row) => (
     |                      ^
  20 |             <li key={row._id}>{row.email} - {row.balance}</li>
  21 |           ))}
  22 |         </ul>

What? 🀨

The wrong turns

Attempt 1 - Type the parameter manually

Fine. You want a type, TypeScript? Here.

rows.map((row: { _id: string; email: string; balance: number }) => ...)

Still broken.

Attempt 2 - Strip the page to the bare minimum

I nuked everything on the page. One query, one <ul>, one .map((row) => <li>{row.email}</li>). 26 lines.

Still broken.

Attempt 3 - Full panic 😡

Ok the bug obviously came from the feature I just added, so let me delete stuff until I find it. I deleted files. Then more files. Then I touched the Convex schema. More deletions. At some point I looked up and I had deleted roughly 70% of the app.

The error was still there.

I genuinely did not understand what was happening anymore.

The actual cause

I ended up opening a stripe.ts file in my editor and I saw it become red.

Deleted it thinking "I'll see later"... And then the build worked again.

What ?? 🀯

The error was not caused by admin/credits/page.tsx. Not by convex/credits.ts. It was in convex/stripe.ts, the file I had just added and never once suspected because npx convex dev showed zero errors. No warnings. Clean terminal. Convex was pretending to be happy...

But stripe.ts had TypeScript errors in it, from the Stripe SDK v20 types. Convex's dev server didn't catch them. The Next.js TypeScript compiler did at build time. And those errors were enough to corrupt the type inference in _generated/api.d.ts, Convex's auto-generated types file. So adminListBalances lost its return type, fell back to any[], and TypeScript screamed at the first .map((row) => it found downstream, which happened to be on that innocent admin page.

Two hours of debugging. The bug was in the one file I never looked at. πŸ™ƒ

Why this is a trap

TypeScript error propagation is non-linear. A broken file silently contaminates the type inference of files that depend on it, and when a codegen step sits in between it's even worse because you'd never think to look there. In Convex, _generated/api.d.ts is generated from your server-side functions, so a type error in any of them can quietly corrupt the return types of every query and mutation in your app.

The error said 'row' implicitly has an 'any' type. That's not the real question. The real question is: why is rows typed as any[]? And for that you need to trace back to whatever generates the return type of adminListBalances and find what's poisoning it.

What to actually do

When you see implicit any on a .map() and the query looks fine:

  1. Don't type the callback parameter manually - you're hiding the problem
  2. Don't bisect the feature you just shipped - the broken file might not be the one you last touched
  3. Look at what generates the API types - in Convex that means all server-side files, especially "use node" ones
  4. Run pnpm convex codegen and read every TypeScript error - not just the Next.js build error. The real one will be there.

Conclusion

Good luck bro. I almost ditched convex tonight.