TS7029Strict Mode
Since TS 1.8

Fix TS7029: Fallthrough Case in Switch

Learn why TypeScript throws TS7029 when a switch case runs into the next one under noFallthroughCasesInSwitch, and how to handle intentional fallthrough.

error TS7029: Fallthrough case in switch

What This Error Means

TS7029 means one of your switch cases can fall into the next one. The case has a body, that body does something, and then execution simply keeps going into the following case label instead of leaving the switch. In JavaScript that is legal — fallthrough is the language's default — which is exactly why TypeScript offers a check for it. Forgetting a break is one of the oldest bugs in the C family of languages.

The check lives behind the noFallthroughCasesInSwitch compiler option. It is not part of strict, so a plain tsc --strict will not report it; you have to enable it in tsconfig.json (or pass --noFallthroughCasesInSwitch). Many templates do this for you — Vite's TypeScript starters and Create React App ship with it on — which is why the error often appears in a project you never configured yourself.

The rule the compiler applies is narrow and worth memorising: a case is reported only when its body is non-empty and the end of that body is reachable. A body that ends in break, return, throw or continue is fine, and so is one ending in a call to a function whose return type is never. An empty case — a bare label stacked above another — is always fine.

Common Causes

All four samples below are compiled with --strict --noFallthroughCasesInSwitch. Without that second flag none of them report anything.

1. A Missing break

The classic. A case does its work and the author moves on to the next label without terminating the current one, so a paid order also gets tracked as shipped.

// ❌ Broken
type OrderStatus = "pending" | "paid" | "shipped"
 
function handleOrder(status: OrderStatus): void {
  switch (status) {
    case "pending":
      break
    case "paid":
//  ~~~~~~~~~~~~~ Error: Fallthrough case in switch. (TS7029)
      notifyCustomer()
    case "shipped":
      startTracking()
      break
  }
}
// ✅ Fixed — terminate the case so it cannot run into "shipped"
function handleOrder(status: OrderStatus): void {
  switch (status) {
    case "pending":
      break
    case "paid":
      notifyCustomer()
      break
    case "shipped":
      startTracking()
      break
  }
}

2. A Return Hidden Inside an if

This one is much easier to miss, because the case body does contain a return — just not on every path. When the condition is false, control reaches the end of the body and drops into the next case.

// ❌ Broken
type RetryMode = "immediate" | "backoff" | "none"
 
function retryDelay(mode: RetryMode, attempt: number): number {
  switch (mode) {
    case "immediate":
//  ~~~~~~~~~~~~~~~~~ Error: Fallthrough case in switch. (TS7029)
      if (attempt < 3) {
        return 0
      }
    case "backoff":
      return attempt * 1000
    case "none":
      return -1
  }
}
// ✅ Fixed — every path out of the case returns
function retryDelay(mode: RetryMode, attempt: number): number {
  switch (mode) {
    case "immediate":
      return attempt < 3 ? 0 : attempt * 1000
    case "backoff":
      return attempt * 1000
    case "none":
      return -1
  }
}

3. Deliberate Fallthrough With a // falls through Comment

Sometimes the fallthrough is on purpose: an error should also be recorded like a warning. ESLint's no-fallthrough rule accepts a // falls through comment as an opt-out. TypeScript does not — its check never looks at comments.

// ❌ Broken — the comment does not silence anything
type LogLevel = "warn" | "error"
 
function report(level: LogLevel): void {
  switch (level) {
    case "error":
//  ~~~~~~~~~~~~~ Error: Fallthrough case in switch. (TS7029)
      pageOnCall()
    // falls through
    case "warn":
      record(level)
      break
  }
}
// ✅ Fixed — hoist the extra step, then stack the empty labels
function report(level: LogLevel): void {
  if (level === "error") {
    pageOnCall()
  }
 
  switch (level) {
    case "error":
    case "warn":
      record(level)
      break
  }
}

Stacked labels with empty bodies are explicitly allowed by the check, so case "error": case "warn": sharing one body compiles cleanly. Only the case that actually does something before falling through is reported.

4. A default Clause That Is Not Last

default does not have to be the final clause, and when it sits in the middle it behaves like any other label: the case above it can fall into it, and it can fall into the case below it.

// ❌ Broken
type Plan = "free" | "pro"
 
function applyPlan(plan: Plan): void {
  switch (plan) {
    case "pro":
//  ~~~~~~~~~~ Error: Fallthrough case in switch. (TS7029)
      grantSeats(25)
    default:
      grantSeats(1)
      break
    case "free":
      grantSeats(1)
      break
  }
}
// ✅ Fixed — move default to the end, where it needs no terminator
function applyPlan(plan: Plan): void {
  switch (plan) {
    case "pro":
      grantSeats(25)
      break
    case "free":
      grantSeats(1)
      break
    default:
      grantSeats(1)
  }
}

A wrapped case body — case "pro": { ... } — does not change this. The braces create a block scope for const and let declarations, but the end of the block is still reachable, so the case still needs a terminator.

How to Fix It

  1. Terminate the reported case. Add break, return, throw or continue at the end of the case body the compiler pointed at — it is reported on the case that falls through, not the one it falls into. This is the right fix nearly every time, because the error is usually a real bug:

    case "paid":
      notifyCustomer()
      break
  2. Check every path, not just the happy one. If the body already ends in a return inside an if, the fallthrough happens when that condition is false. Give the if an else that returns, use a conditional expression, or put a terminator after the block; what matters is that the end of the case body becomes unreachable.

  3. Stack empty labels for shared handling. When several values genuinely share one body, list the labels together instead of letting one body run into another:

    case "error":
    case "warn":
      record(level)
      break
  4. Consider a lookup table instead of a switch. For a dispatch table over a union, a Record keyed by the union removes the whole class of problem — there is no control flow to fall through, and the compiler checks that every member has an entry:

    const orderHandlers: Record<OrderStatus, () => void> = {
      pending: () => {},
      paid: () => notifyCustomer(),
      shipped: () => startTracking(),
    }
     
    orderHandlers[status]()
  5. Keep the flag on. Turning off noFallthroughCasesInSwitch to make one line compile trades a real bug detector for a moment's convenience, and // @ts-expect-error above the next case label is no better. If you need the flag off for a legacy file, prefer keeping it on project-wide and enabling ESLint's no-fallthrough alongside it, which does understand a // falls through comment.

FAQ

What causes TypeScript error TS7029?

TS7029 is reported when noFallthroughCasesInSwitch is enabled and a case clause has a non-empty body whose end is reachable — nothing in it guarantees an exit, so execution can run into the next label. Any of break, return, throw or continue at the end of the body clears the error, and so does a call to a function typed to return never, such as an exhaustiveness helper. Because the option is not part of strict, the same code compiles silently in a project that does not opt in. Vite and Create React App templates enable it by default, which is where most people first meet this error.

How do I allow intentional fallthrough in a TypeScript switch?

There is no per-case opt-out — no comment, no pragma, no directive. The supported pattern is to stack the labels with empty bodies so the shared code sits under all of them:

switch (level) {
  case "error":
  case "warn":
    record(level)
    break
}

If the cases need to do different work before sharing a tail, pull that extra work out into an if before the switch, or extract a helper function that both cases call. As a last resort you can set noFallthroughCasesInSwitch: false for the project, but that disables the check everywhere, not just where you wanted it.

Does TypeScript respect the // falls through comment?

No. // falls through is an ESLint convention, recognised by the no-fallthrough rule, and TypeScript's implementation of this check does not read comments at all — it works purely on the control-flow graph. A case annotated with the comment still reports TS7029, which surprises people migrating a JavaScript codebase that relied on the ESLint rule. If that comment style matters to your team, leave noFallthroughCasesInSwitch off and let ESLint own the check; running both means the comment is honoured by one tool and ignored by the other.

Practice This

Browse all TypeScript practice challenges to keep sharpening your type-level skills.

Related Concepts

Share this reference

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges