Control Flow in Crystal
Learn conditionals, case expressions, and loops in Crystal - from if/else and pattern-style case statements to while, until, and iterators with Docker-ready examples
Control flow determines the order in which your program executes statements - which branches it takes and how often it repeats work. Crystal inherits Ruby’s expressive, readable approach to control flow, but adds compile-time guarantees thanks to its static type system.
The standout characteristic of Crystal’s control flow is that almost everything is an expression that returns a value. An if does not just choose a branch; it evaluates to whatever that branch produces. This lets you assign the result of a conditional directly to a variable, keeping code concise. Crystal also leans on iterators and blocks rather than raw index-based loops, reflecting its multi-paradigm (object-oriented, functional, concurrent) design.
In this tutorial you will learn how to write conditionals with if/elsif/else and unless, branch on values and types with case/when, and repeat work with while, until, and Crystal’s iterator-based loops. You will also see how break and next give you fine-grained control inside loops.
Conditionals: if, unless, and Expressions
Crystal’s if/elsif/else works as you would expect, but remember that it returns a value. There is also unless (the inverse of if), a ternary operator, and a compact suffix form for single-line guards.
Create a file named control_flow.cr:
| |
Because if is an expression, grade is assigned the value of whichever branch runs. The compiler infers its type as String since every branch produces a string. The suffix form (puts ... if ...) reads naturally for short, single-statement conditions.
Branching with case / when
Crystal’s case expression is far more powerful than a C-style switch. A single when can match multiple values, ranges, or even types - and like if, the whole case returns a value. When matching on a union type, case narrows the type inside each branch so the compiler knows exactly what you are working with.
Create a file named case_when.cr:
| |
The array items has the type Array(Int32 | String | Float64), so each item is a union. Inside each when branch, Crystal narrows the type - #{item} is known to be a specific type, which is what makes this both safe and convenient.
Loops: while, until, and Iterators
Crystal supports classic while and until loops, but idiomatic Crystal favors iterators like times and each, which take a block. These avoid off-by-one errors and read clearly. Use break to exit a loop early and next to skip to the following iteration.
Create a file named loops.cr:
| |
Notice that times yields a zero-based index, while a range like (1..3) yields its actual values. In the final loop, next skips every odd number and break halts iteration entirely once the value exceeds 6, so only 2, 4, and 6 are printed.
Running with Docker
You can run all three examples using the official Crystal image without installing anything locally:
| |
Expected Output
Running control_flow.cr produces:
The weather is pleasant
Please log in
Grade: B
Status: adult
Even temperature
Running case_when.cr produces:
It's the weekend!
7 is a single digit
Integer: 42
String: hello
Float: 3.14
Running loops.cr produces:
Count: 1
Count: 2
Count: 3
T-minus 3
T-minus 2
T-minus 1
Iteration 0
Iteration 1
Iteration 2
Number 1
Number 2
Number 3
Color: red
Color: green
Color: blue
Even: 2
Even: 4
Even: 6
Key Concepts
- Everything is an expression -
if,unless, andcaseall return values, so you can assign their result directly to a variable instead of mutating one inside each branch. unlessreads naturally - Use it for the inverse of anifwhen it makes the intent clearer, and prefer the suffix form (do_something if condition) for short single-statement guards.caseis pattern-style matching - A singlewhencan match multiple values, ranges (1..9), or types, making it far more expressive than a traditionalswitch.- Type narrowing in
case- When matching on a union type, Crystal narrows the type inside each branch, giving you compile-time safety with no manual casts. - Prefer iterators over manual counters -
times,each, and ranges with blocks are idiomatic, avoid off-by-one mistakes, and read more clearly than index-based loops. whilevsuntil-whileloops as long as a condition is true;untilloops until a condition becomes true - choose whichever expresses your intent most directly.breakandnext-breakexits the loop immediately, whilenextskips the rest of the current iteration and continues with the following one.
Running Today
All examples can be run using Docker:
docker pull crystallang/crystal:1.14.0
Comments
Loading comments...
Leave a Comment