Friday, May 1, 2026

Untyped case statement

Yet another blog post reminding how useful a compiler could be. Even for projects mainly using typed languages, it is often required to write some code with at least optional types like Typescript/Javascript or Groovy. For the latter of them quite good use case could be to have a jenkins pipeline implemented. This way I have spent some time just having to read more carefully, as what you see may be not exactly how actually things work. The code looked like this:


def aStr = 'x'
switch (aStr) {
  case 'x' || 'y':
    println('x or y')
    break;
  default:
    println('default')
    break;
}

At first glance, it looks like it should match when aStr is either 'x' or 'y'. Right?

Wrong.

The output is:


default

What actually happens?

Similar piece of code landend in my jenkins and I've spent hours trying to find the bug in a code that looks quite good.

The expression 'x' || 'y' doesn't create an "or" condition for the case statement. Instead, it evaluates to a boolean true (since both strings are truthy in Groovy). So the case statement effectively becomes:


case true:

And since aStr is the string 'x', not the boolean true, it never matches. It will always fall through to the default case.

Interestingly, JavaScript has a similar gotcha, though with a twist. In JavaScript, 'x' || 'y' evaluates to 'x' (the first truthy value), so the case becomes case 'x':. This would actually match when aStr is 'x', printing "x or y". But when aStr is 'y', it falls through to default. So the "or" logic still doesn't work as intended - it only matches one of the values. Different language, different behavior, same trap.

Testing with 'y' gives the same result:


def aStr = 'y'
switch (aStr) {
  case 'x' || 'y':
    println('x or y')
    break;
  default:
    println('default')
    break;
}
// Output: default

The correct way

Use a regular expression pattern instead:


def aStr = 'y'
switch (aStr) {
  case ~/x|y/:
    println('expr x or y')
    break;
  default:
    println('default')
    break;
}
// Output: expr x or y

Or use multiple case statements:


switch (aStr) {
  case 'x':
  case 'y':
    println('x or y')
    break;
  default:
    println('default')
    break;
}

Summary

Dynamic typing gives you flexibility, but it won't catch these subtle logical errors at compile time. What looks like an "or" condition is actually a boolean expression that evaluates before the case comparison even happens.

A typed language with proper pattern matching would have caught this at compile time. Or at least made you think twice about what you're actually expressing.

No comments:

Post a Comment