# TypeScript Mistakes That Still Cause Runtime Errors in Production

> TypeScript catches many bugs at compile time — but these common mistakes let real errors slip through to production. Learn what to avoid and why.

- **Published:** 2026-01-11
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** typescript, runtime-errors, type-safety, common-mistakes, production
- **Canonical URL:** https://clixo.sh/blog/typescript-mistakes-that-cause-runtime-errors

TypeScript's value proposition is catching errors before your users do. But TypeScript is a compile-time tool — it erases all type annotations before execution. A false sense of type safety is sometimes worse than no type safety at all, because developers stop writing defensive code.

Here are the TypeScript mistakes that engineers make repeatedly, all of which allow runtime errors to reach production despite a green `tsc` output.

## Common TypeScript Mistakes That Slip Past the Compiler

```mermaid
flowchart TD
  EXT["External Data"] --> BOUND["Trust Boundary"]
  BOUND -->|"as SomeType"| UNSAFE["Typed but Unvalidated"]
  BOUND -->|"schema.parse()"| SAFE["Validated and Typed"]
  UNSAFE -->|"wrong shape at runtime"| CRASH["Runtime Error in Production"]
  SAFE --> APP["Safe Application Logic"]
```

### Casting with `as` Instead of Validating

The most dangerous escape hatch in TypeScript is the type assertion:

```typescript
const user = JSON.parse(response.body) as User;
console.log(user.email.toLowerCase()); // crash if email is missing
```

`as User` does not validate anything. It is a compile-time instruction that tells TypeScript "trust me, this is a User." At runtime, the JSON might be malformed, missing fields, or from an API that changed its schema. TypeScript will not protect you.

The fix is runtime validation at every trust boundary — any data that comes from outside your process:

```typescript
import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

const user = UserSchema.parse(JSON.parse(response.body));
// Now user is actually validated — not just asserted
```

Zod, Valibot, and Arktype all serve this purpose. Pick one and use it consistently at API boundaries, database read paths, and environment variable access.

### Relying on Default Array Index Types

Without `noUncheckedIndexedAccess` in your tsconfig, TypeScript lies to you about arrays:

```typescript
const items: string[] = [];
const first = items[0];
first.toUpperCase(); // TypeScript says string — runtime crash: Cannot read properties of undefined
```

Even with `strict: true`, this compiles cleanly. The flag `noUncheckedIndexedAccess` is separate from the `strict` preset. Without it, `arr[n]` has type `T`, not `T | undefined`. Enable it and fix the downstream errors — they are all real bugs.

### Non-Null Assertions on Values That Can Be Null

The non-null assertion operator `!` is the second most dangerous escape hatch:

```typescript
const el = document.getElementById("app")!;
el.innerHTML = "<p>hello</p>"; // crash if element does not exist
```

The `!` tells TypeScript the value is definitely not null or undefined. If you are wrong, you get a runtime error with no TypeScript warning. Use explicit guards instead:

```typescript
const el = document.getElementById("app");
if (!el) throw new Error("Missing #app element");
el.innerHTML = "<p>hello</p>";
```

The guard is one extra line and makes the failure mode explicit and intentional.

### Ignoring the `unknown` Type in Catch Blocks

With `useUnknownInCatchVariables` (included in `strict: true` since TypeScript 4.4), catch variables have type `unknown`. But many developers immediately cast them:

```typescript
try {
  await fetchData();
} catch (err) {
  console.error((err as Error).message); // crashes if err is a string or network object
}
```

Not every thrown value is an `Error`. External libraries, rejected promises, and legacy code throw strings, plain objects, or custom error types. Check before you cast:

```typescript
try {
  await fetchData();
} catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  console.error(message);
}
```

### Typing Environment Variables as string

`process.env.DATABASE_URL` has type `string | undefined` in TypeScript. A common mistake is to assert it is always defined:

```typescript
const db = new Database(process.env.DATABASE_URL!);
```

If `DATABASE_URL` is missing from a deployment environment, the application will fail at runtime — often with a cryptic error from the database client rather than a clear "missing environment variable" message.

Validate environment variables at startup and fail fast:

```typescript
function requireEnv(key: string): string {
  const value = process.env[key];
  if (!value) throw new Error(`Missing required environment variable: ${key}`);
  return value;
}

const config = {
  databaseUrl: requireEnv("DATABASE_URL"),
  apiKey: requireEnv("API_KEY"),
};
```

Your application either starts correctly or it tells you exactly what is missing. No mystery crashes later.

### Structural Typing Surprises with External Data

TypeScript uses structural typing: if an object has the right shape, TypeScript accepts it. But this means you can accidentally satisfy an interface with an object that has extra (or semantically wrong) fields:

```typescript
interface OrderId {
  value: string;
}

interface UserId {
  value: string;
}

function getOrder(id: OrderId) { /* ... */ }

const userId: UserId = { value: "user-123" };
getOrder(userId); // TypeScript accepts this — they have the same shape
```

For domain identifiers where mixing types is a real bug, use branded types:

```typescript
type OrderId = string & { readonly __brand: "OrderId" };
type UserId = string & { readonly __brand: "UserId" };

function makeOrderId(id: string): OrderId {
  return id as OrderId;
}
```

Now `UserId` and `OrderId` are structurally incompatible at the type level, even though both are strings at runtime.

---

TypeScript is a tool, not a guarantee. The teams that ship reliable software pair it with runtime validation, disciplined config, and code review that checks for these patterns specifically. If your team needs help designing a robust TypeScript architecture, [Clixo](https://clixo.sh/#contact) builds production systems where type safety is treated as infrastructure, not an afterthought.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
