Chapter 6.4☕ 14 min read

Assertion Functions (asserts)

Assert or crash — no soft warnings, only strict narrowing

01The E-Challan System

Custom type guards return a boolean and you use them in if checks. But sometimes you don't want an if — you want the check to ASSERT: "If this condition is false, STOP everything. Throw an error. Crash the program." That's what assertion functions do.

They're like the RTA e-challan system — no warnings, no soft checks. If you violate a rule, the challan is ISSUED immediately. No if (isLegal) { drive() } — you just drive, and if you're illegal, the challan catches you.

asserts functions tell TypeScript: "After this function runs, either the program has crashed (thrown) or the condition is DEFINITELY true." The code after the assert can safely assume the narrowed type. No if-else needed — the assertion itself does the narrowing.

Seedha samjho: type guard = "check and continue if true." Assertion = "check and CRASH if false." Different philosophy, different use case! When you are writing code, you often have assumptions. Maybe you assume a user object from the database will always have an ID. Instead of writing if (!user.id) { throw ... } everywhere, you write assertHasId(user) once. After that line, TypeScript and your runtime both know: if we reached this point, the user has an ID. If not, the program already stopped. This makes your code cleaner and your types narrower, without nesting everything inside if blocks. It is a powerful tool for enforcing invariants in your system, just like how traffic rules are enforced strictly at an RTA checkpost.

02Assertion Function Syntax

There are two forms of assertion functions. (1) Asserting a condition — this tells TypeScript that a specific boolean condition is true after the function runs. (2) Asserting a type — this tells TypeScript that a value is of a specific type after the function runs.

1. Asserting a condition:

function assert(
  condition: unknown,
  message?: string
): asserts condition {
  if (!condition)
    throw new Error(message);
}

After calling assert(x !== null, "x is null"), TypeScript knows x is not null.

2. Asserting a type:

function assertStr(
  val: unknown
): asserts val is string {
  if (typeof val !== "string")
    throw new Error("Not string");
}

After calling assertStr(input), TypeScript knows input is string — no if needed! Compare this to type guards: Type guard → if (isString(input)) { input.toUpperCase() }. Assertion → assertStr(input); input.toUpperCase() — the assertion itself narrows, no if required.

Practical examples:

function assertDefined<T>(
  value: T | undefined
): asserts value is T {
  if (value === undefined)
    throw new Error("Undefined!");
}

function assertHasId(
  obj: unknown
): asserts obj is { id: string } {
  // check logic here
}

The key insight: assertions narrow by ELIMINATING the false path (it throws), so only the true path remains. This is incredibly powerful when you want to guarantee that a value matches a certain shape before proceeding with your main logic. Instead of wrapping your entire function body in an if block, you place the assertion at the top, and the rest of the function can proceed with full type safety. The false path is handled by the assertion itself, which throws an error and stops execution, ensuring your code never reaches the next line if the condition isn't met.

03Assertion vs Type Guard

Let's compare side by side. Type guard: returns boolean, used in if/else, both branches continue, safer (doesn't throw). Assertion: throws on failure, used standalone, no branching needed, more concise but more dangerous (throws in production).

When assertions are better:

(1) Input validation at function boundaries — function processUser(data: unknown) { assertIsUser(data); /* data is User from here */ }.

function processUser(data: unknown) {
  assertIsUser(data);
  // data is User here!
  data.name; // safe
}

(2) Configuration validation at startup — assert(config.port, "Port is required");. (3) Test assertions — function expect<T>(value: T): asserts value is NonNullable<T> { if (value == null) throw new AssertionError(); }.

When type guards are better:

(1) Handling multiple valid paths — if (isSuccess(res)) { ... } else { handleError(res); }. (2) When throwing is too aggressive — you want to recover, not crash. (3) Filtering arrays — assertions don't work with .filter().

// Type guard for branching
if (isSuccess(res)) {
  handleSuccess(res);
} else {
  handleError(res);
}

// Type guard for filtering
const nums = arr.filter(
  isNumber
);

Node.js's built-in assert module returns void and can be used as an assertion function with proper typing. Assertions are essentially a way to enforce "fail-fast" behavior. If something unexpected happens, you want to know immediately rather than letting the program continue in a bad state. However, this comes with the responsibility of ensuring you only assert things that truly indicate a bug if false. Throwing an exception because a user entered the wrong password is a bad experience; throwing because a required config variable is missing at startup is exactly right.

04Assertion Traps

Even experienced developers trip over these assertion function gotchas. Let's make sure you avoid every single one of them!

Trap 1: Assertion function that doesn't throw.

// ❌ Forgot to throw!
function badAssert(
  val: unknown
): asserts val is string {
  // oops, no throw!
}

TypeScript TRUSTS that you throw on failure. If you don't throw, the narrowed type is WRONG and you get runtime errors. Always ensure your assert function throws when the condition is false!

Trap 2: Using assertions when you should handle the error.

Crashing in production because you asserted something that might legitimately fail is BAD. Use assertions for things that SHOULD NEVER happen (programming errors), not for things that might happen (user input, network failures).

Trap 3: assert never vs assert is.

asserts value is Type narrows the type. asserts condition just asserts a boolean condition. Don't confuse them.

Trap 4: Assertion in async code.

// ❌ Doesn't work!
async function assertFetch(
  url: string
): Promise<asserts res is Response>

You can't use asserts with async functions that return something other than void.

Trap 5: Missing return type.

// ❌ Missing return type!
function assertString(val: unknown) {
  if (typeof val !== "string")
    throw new Error();
  // TS doesn't narrow outside!
}

// ✅ Fix: Must add return type
function assertStr2(
  val: unknown
): asserts val is string {
  if (typeof val !== "string")
    throw new Error();
}

Without the asserts val is string return type, TypeScript DOESN'T narrow. The compiler does not analyze the body of the function to infer that it's an assertion function. The asserts keyword in the return type is the only way to activate this special narrowing behavior. If you forget it, you just have a normal function that might throw, but TypeScript will not narrow the type after calling it.

05Assertion Functions Cheatsheet

Here's your quick-reference cheatsheet for assertion functions — pin it to your mental dashboard and refer back whenever you need it!

  • Condition assert: function assert(c, m?): asserts condition
  • Type assert: function fn(x): asserts x is Type
  • Usage: Call the function; narrowing happens automatically. No if needed.
  • Type guard vs assertion: guard = boolean + if, assertion = throw + standalone
  • When to assert: (1) Things that should NEVER be false, (2) Input validation at boundaries, (3) Test expectations, (4) Startup config checks
  • When NOT to assert: (1) Things that might legitimately fail, (2) User input validation (handle gracefully instead), (3) Array filtering (use type guards)
// Condition assertion
function assert(
  cond: unknown,
  msg?: string
): asserts cond {
  if (!cond) throw new Error(msg);
}

// Type assertion
function assertStr(
  val: unknown
): asserts val is string {
  if (typeof val !== "string")
    throw new Error("Not string");
}

Key rules:

  • Always throw in the false path
  • Must include the asserts return type
  • Trust but verify — your assertion logic must be correct

The Golden Rule: Assertions are the e-challan — violate and you're STOPPED. Use them for things that should NEVER be wrong. Don't e-challan the user, e-challan the code, bhai!

Key Takeaways

  • Assertion functions narrow types by throwing errors instead of returning booleans
  • Use `asserts condition` to assert a boolean, `asserts x is Type` to narrow a value
  • You MUST include the `asserts` return type — without it, TypeScript will not narrow
  • Use assertions for things that should NEVER be false, not for expected user errors
  • Always ensure your assertion function actually throws in the false path
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase