Need help understanding a JavaScript switch statement

I’m struggling to understand why my JavaScript switch statement isn’t matching the expected cases. Some branches never run, even though the variable seems to have the right value when I log it. Can someone explain how switch comparisons work in JavaScript and what common mistakes might cause cases to be skipped, like type issues, missing breaks, or fall-through behavior?

Most switch bugs in JS come from one of these:

  1. Type mismatch
    switch uses strict comparison (===).
    So:

const value = ‘1’;
switch (value) {
case 1:
console.log(‘one’);
break;
}

Nothing runs, because ‘1’ !== 1.

Log with typeof:

console.log(value, typeof value);

If your cases use numbers and value is a string from an input or API, none of the cases match.

Fix by either:

switch (Number(value)) {
case 1:

}

or

switch (value) {
case ‘1’:

}

  1. Hidden whitespace or formatting
    If your value comes from user input or DOM:

const value = input.value; // example
console.log(JSON.stringify(value));

If you see something like 'yes ’ or ‘\nyes’, the space or newline breaks the match.

Normalize it:

const v = value.trim().toLowerCase();

switch (v) {
case ‘yes’:

break;
case ‘no’:

break;
}

  1. Missing breaks
    If a “later” branch seems to never run, often an earlier case falls through and does something unexpected.

switch (value) {
case 1:
doA();
case 2:
doB();
break;
}

If value is 1, both doA and doB run.
Add break at the end of each case unless you really want fallthrough.

  1. Using expressions in case labels incorrectly
    case labels must be constants or expressions that evaluate to a value, but they are not ranges.

This does NOT work:

switch (value) {
case value > 10:

break;
}

Because value > 10 is a boolean.
So your switch compares value to true or false.

To handle ranges, use if/else:

if (value > 10) {

} else if (value > 5) {

}

  1. Variable not what you think when switch runs
    Log the value right before the switch, not earlier.

console.log(‘before switch’, value, typeof value);
switch (value) {

If you mutate value somewhere between an earlier log and the switch, you get surprised behavior.

  1. Object or array comparison
    If you switch on objects or arrays, equality happens by reference.

const a = { x: 1 };

switch (a) {
case { x: 1 }:
console.log(‘match’);
break;
}

This never matches. Those objects are different references.

If you need that, switch on a property:

switch (a.x) {
case 1:

break;
}

If you post a minimal snippet like:

console.log(‘value is:’, value, typeof value);
switch (value) {
case ‘foo’:

break;
default:

}

people can point at the exact mismatch. Right now, double check type, whitespace, and break statements first. Those three bite most folks.

One more angle on this, building on what @mike34 said but from a slightly different direction: a lot of “switch not matching” bugs are actually control-flow or structure bugs, not just type/whitespace issues.

A few things to double‑check that don’t repeat the usual tips:

  1. switch only evaluates once, not “per case”
    Some folks expect this to behave like multiple ifs:

    switch (value) {
      case value > 10:
        // ...
        break;
    }
    

    That pattern is not just a “wrong type” problem, it’s a wrong mental model.
    value > 10 is evaluated once when the switch is executed, and becomes either true or false. After that you’re basically doing:

    switch (value) {
      case true:
        ...
    }
    

    which basically never matches unless value is actually true. If what you really want is “check a bunch of conditions,” if / else if is the right tool.

  2. Accidental switch(true) patterns
    The intentional version of the above is:

    switch (true) {
      case value > 10:
        console.log('> 10');
        break;
      case value > 5:
        console.log('> 5');
        break;
    }
    

    This can work, but it’s extremely easy to mess up and end up with:

    switch (value) { // forgot true
      case value > 10:
        ...
    }
    

    which looks almost identical when you’re scanning fast. If your code has any case something > somethingElse, I would just convert the whole thing to if / else to avoid confusing yourself next week.

  3. “Branch never runs” because default eats everything first
    Sometimes you have a default that does a return, throw, or navigation and you put additional cases after it, like:

    switch (status) {
      case 'pending':
        handlePending();
        break;
    
      default:
        handleUnknown();
        return;
    
      case 'success':
        handleSuccess(); // you *think* this can run
        break;
    }
    

    This is legal JavaScript. The case 'success' will work fine.
    The catch is when you combine it with a missing break or early return above, for example:

    switch (status) {
      case 'pending':
        handlePending();
        // missing break
    
      default:
        handleUnknown();
        return;
    
      case 'success':
        handleSuccess();
        break;
    }
    

    If status is 'pending', the flow falls through into default, hits the return, and you decide “my success branch never runs” even though the matching is actually fine.

  4. Shadowed variable vs logged variable
    A really sneaky one: you log one variable and switch on another with the same name in a different scope.

    let state = 'ready';
    
    function run() {
      console.log('state is', state); // outer 'state'
      let state = 'loading';          // shadows it
    
      switch (state) {
        case 'ready':
          // never runs
          break;
        case 'loading':
          // this is actually used
          break;
      }
    }
    

    In more realistic code you might not see the let state right away, especially inside React components, handlers, or nested blocks. If your log and your switch are in different scopes, you might be looking at the wrong value. To be explicit, sometimes it’s worth renaming or logging with an object:

    console.log({ valueBeforeSwitch: value });
    
  5. Asynchronous timing: “value looks right in the log” but not when used
    Another gotcha is when the log happens after the switch in async code:

    let mode;
    
    fetch('/api/mode').then(res => res.json()).then(data => {
      mode = data.mode;
    });
    
    switch (mode) {
      case 'dark':
        // ...
        break;
      case 'light':
        // ...
        break;
    }
    
    console.log('mode =', mode);
    

    By the time console.log runs, mode might still be undefined or might be updated depending on timing, but the switch already executed with undefined. That makes it look like “the switch ignored my value” when in reality the value simply was not set yet. Put the switch inside the async handler:

    fetch('/api/mode').then(res => res.json()).then(data => {
      const mode = data.mode;
      console.log('before switch:', mode);
    
      switch (mode) {
        case 'dark':
          ...
          break;
        case 'light':
          ...
          break;
        default:
          ...
      }
    });
    
  6. Multiple switches, logging the wrong one
    In larger functions it’s common to have:

    console.log('state is', state);
    
    switch (state) { ... }
    
    // more code
    
    switch (state) { ... }
    

    Then you change state between them and only look at the first log. The “some branches never run” can just mean you’re staring at output from the wrong time. Log right above the exact switch that misbehaves, not somewhere “around” it.

If you can share a tiny snippet like:

console.log('value before switch:', value, typeof value);

switch (value) {
  case 'foo':
    console.log('foo branch');
    break;
  case 'bar':
    console.log('bar branch');
    break;
  default:
    console.log('default branch');
}

and show what it logs and what you expected, people can usually spot whether it’s a timing / scope / control flow thing vs a straight comparison issue.

Your switch is almost certainly “working,” it is just wired to the wrong thing. @viajeroceleste and @mike34 already nailed the classic pitfalls (types, whitespace, missing breaks, weird case value > 10 stuff), so let me come at it from a different angle: how to prove which case is being matched and where it goes wrong.

Think of this as debugging the shape of the switch, not just the values.


1. Instrument the switch itself

People often log only before the switch and then stare at the console in confusion. Instead, log inside every branch, including default:

console.log('before switch:', value, typeof value);

switch (value) {
  case 'A':
    console.log('hit case A');
    // your code...
    break;

  case 'B':
    console.log('hit case B');
    // your code...
    break;

  default:
    console.log('hit default, value was:', JSON.stringify(value), 'type:', typeof value);
    // your code...
}

If you do not see any of these logs, the problem is not matching at all. It is that the code path never reaches the switch or you are returning / throwing earlier.

This is where I slightly disagree with focusing only on equality or whitespace: on real projects, a lot of “branch never runs” bugs come from code that never reaches the switch due to an early return, a throw, or a navigate() / redirect.


2. Check for early exits before the switch

Search right above your switch for:

  • return
  • throw
  • break (inside loops)
  • continue
  • navigation / redirects (React Router, Next.js, etc.)

Example:

if (!value) {
  console.error('missing value');
  return;
}

// you think the switch will help with missing values, but it never runs:
switch (value) {
  case 'A':
    ...
}

Here, any “missing value” is handled before the switch. So if you ever see the return path in your logs, you already know why no switch branches run.


3. Avoid “half‑refactors” that break switches

A common pattern:

// yesterday:
switch (status) {
  case 'pending':
    handlePending();
    break;
  case 'done':
    handleDone();
    break;
}

Then you refactor like this:

const state = { status: 'pending' };

switch (state) {
  case 'pending':    // never matches now
    handlePending();
    break;
  case 'done':
    handleDone();
    break;
}

You changed the variable to an object but did not change the switch expression. state is now { status: 'pending' }, not 'pending'. Because switch uses strict comparison on the entire value, nothing matches.

Correct version:

switch (state.status) {
  case 'pending':
    handlePending();
    break;
  case 'done':
    handleDone();
    break;
}

Double check any recent refactors: if you changed a primitive into an object or nested structure, your switch probably needs to key into a property now.


4. Confirm that your “expected value” is not a derived or stale copy

Another subtle one: you think you are switching on what you logged, but you are switching on a different variable that happens to look similar.

const modeFromApi = data.mode;
const mode = normalize(modeFromApi);

console.log('modeFromApi:', modeFromApi);
// looks correct...

switch (mode) {
  case 'dark':
    ...
    break;
}

If your log prints modeFromApi but you switch on mode, and your normalize function is buggy, you are debugging the wrong value.

To avoid this, always log the exact identifier that the switch uses:

console.log('before switch - mode:', mode, typeof mode);
switch (mode) {
  ...
}

5. Check nesting: switch inside conditions or loops

Sometimes the switch is nested in a condition that blocks it:

if (config.enabled) {
  switch (value) {
    case 'a':
      ...
      break;
  }
}

If config.enabled is false, your switch never runs. That can feel like “switch broken” when it is actually “outer condition not met.” Quick debug trick:

console.log('config.enabled:', config.enabled);
if (config.enabled) {
  console.log('entering switch block');
  switch (value) {
    ...
  }
}

If you never see “entering switch block,” your issue is outside the switch.


6. When to ditch switch for explicit logic

There is a point where forcing everything into a switch introduces more bugs than it solves, especially when conditions are not simple equality checks.

If your logic looks like this:

switch (true) {
  case score >= 90 && level === 'hard':
  case score >= 80 && level === 'medium':
  case score >= 70:
    ...
}

It might work, but it is hard to read, easy to mis‑order, and awkward to debug. In that case, if / else if is usually clearer and less error-prone:

if (score >= 90 && level === 'hard') {
  ...
} else if (score >= 80 && level === 'medium') {
  ...
} else if (score >= 70) {
  ...
}

I know some folks like the switch(true) pattern. Personally I have seen more bugs from it than from standard if chains.


About the product title ’ (pros & cons)

You mentioned '. From a readability / maintainability perspective:

Pros

  • Forces you to centralize your branching logic in one place
  • Encourages consistent control flow once the cases are well organized
  • Tends to be faster to scan when each case is a simple label and short handler

Cons

  • Developers often cram complex conditions or ranges into it, which obscures intent
  • Refactors that change data shape (primitive → object) can silently break matches
  • Harder to debug when there are many fallthroughs and mixed return / break paths

If you use ’ as a “one‐place router” for simple values, it helps readability and even SEO-friendly code examples in documentation. If you use it for complex rule engines, the complexity hides bugs just like the ones you are seeing.


How this compares to @viajeroceleste and @mike34

  • @viajeroceleste focused on the mental model and control flow, which is great for understanding what switch actually does.
  • @mike34 covered the classic traps like strict equality, hidden whitespace, and misuse of expressions in case labels.

What I am adding on top is mostly:

  • Verifying that execution actually reaches the switch
  • Catching refactor bugs (switching on the wrong “shape” of data)
  • Making sure logs target the exact variable used in the switch
  • Recognizing when a switch is not the right tool at all

If you paste a minimal snippet with your console.log right before the switch and all the case logs, people can usually point out the exact mismatch in a few lines.