TypeScript Errors Are The Only Thing Saving Your Financial App

Actually, I used to treat TypeScript errors like suggestions. You know the vibe: “Yeah, I know this could be undefined, but I promise it won’t be.” I’d slap a non-null assertion operator (!) on it and call it a day. Ship it.

But then I built a payroll system. Suddenly, that little red squiggly line wasn’t just a linter being annoying—it was the only thing standing between me and accidentally paying a contractor $NaN or, worse, double-paying them because a retry loop didn’t account for a specific edge case.

We need to talk about error handling. Not the “catch generic error and log it” kind. I mean the deep, structural type safety that prevents your application from swallowing invalid states until they explode in a user’s face. If you’re building anything with money—invoicing, burn rate calculators, Stripe integrations—TypeScript isn’t just a tool. It’s your insurance policy.

The “Impossible” State is Guaranteed to Happen

Here’s a scenario I ran into last month. We were calculating monthly burn rates for a startup dashboard. Simple math, right? Revenue minus expenses.

Except, what happens when revenue is zero? Or worse, what happens when the API returns a string “0.00” instead of a number because some legacy PHP service decided to get involved?

In standard JavaScript, "1000" - "500" works (thanks, coercion). But "1000" + "500" gives you "1000500". If you aren’t strictly typing your inputs, you just inflated a company’s runway by a factor of a thousand. I’ve seen this happen.

// Don't do this
type FinancialRecord = {
  amount: number;
  currency: string;
};

// Do this: Branded Types
declare const __brand: unique symbol;
type Brand<K, T> = K & { [__brand]: T };

type Money = Brand<number, 'Money'>;
type CurrencyCode = Brand<string, 'CurrencyCode'>;

function createMoney(amount: number): Money {
  // Runtime validation anchor
  if (!Number.isSafeInteger(amount)) {
    throw new Error(Precision loss detected: ${amount}. Handle cents as integers!);
  }
  if (amount < 0) {
     // Is negative money allowed? Maybe for refunds, but not for revenue.
     // Context matters.
  }
  return amount as Money;
}

function calculateBurn(revenue: Money, expenses: Money): Money {
  return (revenue - expenses) as Money;
}

// Usage
const rev = createMoney(500000); // $5000.00
const exp = createMoney(800000); // $8000.00

// TS Error: Argument of type 'number' is not assignable to parameter of type 'Money'.
// calculateBurn(100, 200); 

const burn = calculateBurn(rev, exp); 
// Result: -300000 (Negative burn means profit, or debt? Handle the edge case!)

Trusting External APIs is a Fool’s Errand

I was integrating a webhook handler recently—standard stuff, listening for payment success events. The documentation said the payload would look one way. The production environment disagreed.

If you type your API responses manually (e.g., response as PaymentData), you are lying to TypeScript. You are telling the compiler “I checked this,” when you absolutely did not check it. When the API changes the schema without telling you (looking at you, random SaaS providers), your app crashes at runtime with Cannot read property of undefined.

import { z } from 'zod';

// Define the schema based on what we actually need
const StripeEventSchema = z.object({
  id: z.string(),
  type: z.enum(['payment_intent.succeeded', 'payment_intent.payment_failed']),
  data: z.object({
    object: z.object({
      amount: z.number().int().positive(), // Catch negative amounts immediately
      currency: z.string().length(3),
      metadata: z.record(z.string()).optional(),
    }),
  }),
});

async function handleWebhook(payload: unknown) {
  // parse() throws if the data doesn't match. 
  // safeParse() returns a result object. I prefer safeParse for webhooks
  // so I can return a 400 instead of crashing 500.
  const result = StripeEventSchema.safeParse(payload);

  if (!result.success) {
    console.error("Webhook payload rejected:", result.error.format());
    // Return specific error to the provider so they stop retrying if it's a bad payload
    return { status: 400, error: 'Invalid Payload Structure' };
  }

  const event = result.data;
  
  // Now 'event' is fully typed, and we KNOW the data is valid.
  // We can safely do math on event.data.object.amount
  
  if (event.type === 'payment_intent.payment_failed') {
     // Trigger retry logic or user notification
     await notifyUser(event.data.object.metadata?.userId);
  }
  
  return { status: 200 };
}

The Async/Await Trap

Here is something that bugs me about TypeScript in 2026: the catch block is still a lawless wasteland. When you catch an error, it is typed as any or unknown. You lose all context.

type Result<T, E = string> = 
  | { success: true; data: T }
  | { success: false; error: E };

// A safe wrapper for risky financial calculations
async function calculateGrowth(
  current: number, 
  previous: number
): Promise<Result<number>> {
  
  try {
    // Edge case: Division by zero
    if (previous === 0) {
      // Is this an error? Or is growth infinite? 
      // For a dashboard, infinite is bad UX. Let's return a specific error.
      return { success: false, error: 'cannot_calculate_growth_from_zero' };
    }

    const growth = ((current - previous) / previous) * 100;
    
    // Simulate an async DB call or AI prompt validation
    await new Promise(r => setTimeout(r, 10));

    return { success: true, data: growth };

  } catch (e) {
    // Unknown error boundary
    console.error("Unexpected calculation error", e);
    return { success: false, error: 'internal_calculation_failed' };
  }
}

// Usage in a component
async function DashboardComponent() {
  const result = await calculateGrowth(1500, 0);

  if (!result.success) {
    // TypeScript knows 'result.error' exists here
    if (result.error === 'cannot_calculate_growth_from_zero') {
      return N/A (New Revenue);
    }
    return Error;
  }

  // TypeScript knows 'result.data' exists here
  return {result.data.toFixed(2)}%;
}

Don’t Let AI Write Your Error Logic

I use AI assistants daily—they’re great for boilerplate. But I’ve noticed a dangerous trend. If you ask an AI to “handle errors” for a financial function, it almost always gives you a generic try/catch that logs to console and returns null.

In financial software, null is ambiguous. Does it mean $0? Does it mean “data missing”? Does it mean “calculation failed”?

Be specific. If the growth calculation is invalid because of negative burn, throw a specific error code like INVALID_INPUT_NEGATIVE_BURN. Make the UI handle it. Don’t hide the mess.

The Payoff

Strict typing feels like swimming in jeans when you’re prototyping. I get it. You just want to see the data on the screen.

But the moment you move from “pet project” to “people’s livelihoods rely on this,” those errors become your best friends. I’d rather fight the compiler for ten minutes on a Tuesday afternoon than fight a database corruption issue at 2 AM on a Saturday.

If you aren’t validating your edges—webhooks, user inputs, database reads—you aren’t really using TypeScript. You’re just using fancy JavaScript with better autocomplete. TypeScript Strict Mode is a non-negotiable requirement for any production-ready application.

Mateo Rojas

Learn More →

Leave a Reply

Your email address will not be published. Required fields are marked *