Crystal — Reference
Source: https://crystal-lang.org/reference/
Crystal
- Created: first public release 2014-06-19 by Ary Borenszweig, Juan Wajnerman, and Brian Cardiff at Manas Technology Solutions, Buenos Aires (Wikipedia).
- Latest stable: Crystal 1.20.1 (April 2026) (crystal-lang.org/reference).
- Owner: Crystal core team, sponsored by Manas and 84codes (long-time corporate sponsor; runs CloudAMQP/CloudKarafka in Crystal). License: Apache 2.0.
- Paradigms: object-oriented (everything is an object), with functional flavor; metaprogramming via macros.
- Typing: static, strong, with global type inference and union types. Type annotations rarely required.
- Memory: garbage-collected (Boehm-Demers-Weiser conservative GC).
- Compilation: AOT to native via LLVM. Single static binary by default (with
--staticagainst musl). - Primary domains: web backends, CLI tools, network services, microservices — anywhere “Ruby ergonomics, C performance” is the pitch.
- Official docs: https://crystal-lang.org/reference/
At a glance
Crystal reads like Ruby — same blocks, same .each, same def end syntax — but compiles ahead-of-time via LLVM, statically type-checks the entire program with global inference (you almost never write a type), and uses union types for flexibility (Int32 | String is a real first-class type). Concurrency is fibers + channels (Go-style CSP) on a Boehm GC. Multi-threading exists in preview (-Dpreview_mt) and is heading to default. The shards package manager and the Kemal/Lucky frameworks make web work fast.
Getting started
Install:
- macOS:
brew install crystal. - Linux: official repos at https://crystal-lang.org/install/.
- Windows: experimental but functional (since 1.0); use the WSL or native installer.
asdf-crystalplugin if you use asdf.
Hello world (file hello.cr):
puts "Hello, world!"Compile + run: crystal run hello.cr (compile to temp + execute), or crystal build hello.cr (produces ./hello binary).
REPL: icr (community Interactive Crystal). No first-class REPL — the AOT compilation model fights it. The idiomatic interactive workflow is crystal play (web playground) or crystal run on small files.
Project layout (a shard):
mypkg/
shard.yml # manifest
shard.lock # lockfile
src/
mypkg.cr # entry point
mypkg/
submod.cr
spec/ # tests (Crystal's spec framework)
mypkg_spec.cr
bin/ # compiled outputs (gitignored)
README.md
Initialize with crystal init app myapp or crystal init lib mylib.
Package/build tool: shards (ships with Crystal). shards install reads shard.yml, populates lib/, writes shard.lock. Registry is implicit (Git-based) — depend on a GitHub repo + version directly. crystal build src/mypkg.cr -o bin/myapp --release for production.
Basics
Types and literals:
- Integers:
Int8/16/32/64/128,UInt8/16/32/64/128. Default literal42isInt32. Suffixes:42_i64,42_u8. - Floats:
Float32,Float64(default for1.0). Suffix:1.0_f32. Bool,Char(single Unicode codepoint, 4 bytes),String(UTF-8, immutable),Symbol(:foo, interned).Nil(singleton, typeNil). Optional pattern:Int32?(=Int32 | Nil).- Composites:
Array(T)([1, 2, 3]),Hash(K, V)({:a => 1}or{a: 1}),Tuple(T1, T2, ...)({1, "a"}— fixed-length, heterogeneous),NamedTuple(a: Int32, b: String),Set(T),Range,Slice(T)(pointer + size, for binary),Pointer(T)(raw, FFI/unsafe).
Variables/scoping: assignment with = (no declaration keyword for locals). CONSTANT = ... (uppercase) is module-level constant. Scopes: top-level, class, def, block. Methods cannot access enclosing locals; blocks (do |x| ... end or { |x| ... }) capture them.
Control flow: if/elsif/else/end, unless, case ... when ... end (pattern + type matching), while, until, loop, break, next. Everything is an expression (x = if cond then 1 else 2 end).
Functions (called methods even at top level — they’re methods of Object):
def add(x, y)
x + y # last expression returned
end
def greet(name : String = "world") : String
"Hello, #{name}"
end
# Type annotations optional; full inference covers most cases
greet # "Hello, world"
greet("Crystal")Method overloading by arity AND type: def f(x : Int32) and def f(x : String) are distinct methods. Splat: def f(*args). Block param: def each(&block); yield ... end.
Strings: UTF-8, immutable, properly Unicode-aware ("abc".size is grapheme/char count). Interpolation: "x = #{x + 1}". Concat: "a" + "b". Multi-line heredoc:
s = <<-HEREDOC
Multi
line
HEREDOCCollections: [1, 2, 3] is Array(Int32). [] of String for empty typed. Array.new(3, "x") to fill. Hash: {"a" => 1} (= Hash(String, Int32)). Tuple: {1, "a", true} is Tuple(Int32, String, Bool) — distinct type per arity/types.
Intermediate
Type system depth:
- Union types:
Int32 | String | Nilis a first-class type. Branching narrows: insideif x.is_a?(String),xis typedString. - Generic classes:
class Box(T); end. Multiple params allowed. - Modules as mixins:
module Comparable; ... end, thenclass Foo; include Comparable; end(instance methods) orextend(class methods). - Abstract methods:
abstract def name : Stringin abstract classes/modules. - Inheritance: single inheritance + module mixins.
Foo < Bar. - Type restrictions in method sigs are checked at compile time and used for overload resolution.
Modules: namespace via module M; ... end. File and module names are separate (one file may contain many; one module may span many files). require "./other" for relative file include; require "json" for stdlib; require "kemal" for shard. Public/private: private def, private class, protected.
Error handling: exceptions, like Ruby. raise "msg" (raises Exception), raise CustomError.new(...). begin ... rescue Foo ... ensure ... end. Inline rescue: value = risky() rescue default. Custom errors: class MyError < Exception; end. Result types are not built-in; Result(T, E) exists in shards.
Concurrency primitives:
- Fibers (lightweight, M:1 by default, M:N with
-Dpreview_mt):spawn { do_work }. Fiber stacks are small (4KiB initial). Cooperative scheduling — yields on I/O via the event loop. - Channels:
ch = Channel(Int32).new(unbuffered) orChannel(Int32).new(10)(buffered).ch.send 1;x = ch.receive.selectfor multiplexing. - Atomics:
Atomic(Int32).new(0),compare_and_set,add. - Mutex for locking shared state under
-Dpreview_mt.
I/O: File.read, File.write, File.open(path, "r") { |io| ... }. IO is an abstract base; IO::Memory for in-memory, IO::FileDescriptor for OS handles. STDIN/STDOUT/STDERR. Networking: TCPSocket, UDPSocket, HTTP::Server, HTTP::Client. JSON via JSON.parse / JSON::Serializable (mixin auto-derives parsers).
Stdlib highlights: Array, Hash, Set, Tuple, NamedTuple, Slice, Time/Time::Span, JSON, YAML, XML, URI, HTTP::Server/HTTP::Client, Socket, OpenSSL, OAuth2, Log, Spec (test framework), Regex, Math, Random, Crypto, Digest, Base64, File, Dir, Process, ENV, Channel, Mutex.
Advanced
Memory / GC: Boehm-Demers-Weiser conservative GC ships with the runtime. Conservative because it scans the stack as bytes. Pros: simple, no compiler-side ownership tracking; works for FFI. Cons: occasional false retentions, no compaction. Allocations of heap-managed objects (classes) go to the GC; Struct types are stack-allocated value types with no GC overhead. GC.collect to force; GC.stats for telemetry.
Concurrency deep dive:
- Default single-threaded fibers + event loop (epoll/kqueue/IOCP).
- Multi-thread mode:
-Dpreview_mt. Spawns N worker threads, scheduler distributes fibers. Channels andAtomicare MT-safe; many other types (e.g.,Hash) are not — wrap withMutex. - Path to default MT: tracked in core team RFCs; expect default ON in 2.x.
- No async/await colored functions — fibers + blocking I/O calls is the model. Code looks synchronous.
FFI: first-class via lib blocks.
@[Link("c")]
lib LibC
fun puts(s : LibC::Char*) : LibC::Int
struct Tm
tm_sec : Int32
tm_min : Int32
# ...
end
end
LibC.puts("hello")fun declares C functions; struct/union/enum mirror C types. @[Link] controls library resolution. Pointer ops: ptr.value, ptr[i], ptr + 4. Crystal’s Slice(UInt8) interops with UInt8* + length cleanly.
Reflection: limited at runtime (no MOP-style introspection like Ruby), substantial at macro time — see God mode.
Performance tools:
--releaseenables LLVM optimizations; build always with--releasefor benchmarks (debug build is much slower).crystal build --release --no-debugfor prod.--statsand--timeflags show compile phase costs.valgrindandperfwork on the resulting binary.Benchmark.ips { |x| x.report("name") { ... } }for micro-benchmarks.
God mode
Macros = compile-time AST manipulation, written in a Crystal-like macro language. Macros run at compile time, take AST nodes, emit code:
macro define_setters(*names)
{% for name in names %}
def {{name.id}}=(v)
@{{name.id}} = v
end
{% end %}
end
class Foo
define_setters x, y, z
endThe {% %} blocks evaluate at compile time; {{ }} interpolates. .id, .stringify, .types are AST methods. {% if @type.has_method?(:foo) %} introspects the surrounding type. JSON::Serializable and many ORM mappers are pure macros.
method_missing macro: define a macro method_missing(call) ... end to intercept unknown method calls at compile time (unlike Ruby’s runtime method_missing). Used by DB::Mapper, dynamic-feel libraries.
Type inference + union types: the compiler runs whole-program inference. After x = cond ? 1 : "s", x : Int32 | String. Calling x.upcase is a compile error unless you narrow first (if x.is_a?(String)). This is unusual — most “dynamic-feel” languages let you compile and crash at runtime.
Fibers + scheduler internals: each fiber has its own stack (4KiB grows on demand). The scheduler is in Crystal::Scheduler; you can implement custom event loops by replacing Crystal::EventLoop. Stack switching uses ucontext-style assembly.
Bindings to C via lib: the lib block plus @[Link] annotations is the only FFI mechanism; no header parsing. Tools like crystal_lib auto-generate bindings from C headers. Many shards (sqlite3, PCRE, OpenSSL) are just lib declarations.
Boehm GC tricks: GC.malloc_atomic for blocks containing no pointers (the GC won’t scan inside) — used internally for String, byte arrays. GC.set_finalizer(obj) { ... } for cleanup hooks (rarely needed).
-D flags / compile-time conditions: -Dpreview_mt, -Dwithout_openssl, custom flags via {% if flag?(:my_flag) %}. Selects code paths at compile time.
Static binaries: crystal build --release --static --link-flags='-static-pie' against musl-libc Alpine container produces a fully-static binary suitable for FROM scratch Docker. The killer deployment story.
Embedding tricks: {{ read_file("data.json") }} embeds a file’s contents at compile time. {{ run("./gen.cr") }} shells out at compile time and embeds output (similar to a build-script).
Idioms & style
- Naming:
snake_casemethods/vars,CamelCaseclasses/modules,SCREAMING_SNAKEconstants. Predicate methods end in?(empty?); mutating methods end in!(reverse!). - Formatter:
crystal tool format(built-in). No options to argue. - Linter: Ameba (
crenv plugin add amebaor via shard). The de facto style/quality checker. - Idiomatic patterns:
- Avoid type annotations unless needed for clarity, overload resolution, or instance-var typing.
- Use
Structfor small immutable value types. - Prefer modules +
includeover deep inheritance. - Use
JSON::SerializableandYAML::Serializablemixins instead of hand-writing parsers. - Use
Specfor tests; one_spec.crper file. - Prefer
&.methodfor nil-safe calls (obj.try &.method).
- Expert review focus: union type explosion (large
T | U | ...slows compile and obscures intent), missing@instance_varstype declarations (compiler infers from constructor — sometimes wrongly), uncovered abstract methods, fiber starvation (CPU loops withoutFiber.yield), shadowing block params, macro hygiene (use%-prefixed gensym vars).
Ecosystem
- Web: Kemal (Sinatra-clone, dominant), Lucky (full-stack, Rails-shaped), Athena (Symfony-inspired), Amber (Rails-inspired), Marten (Django-inspired),
HTTP::Server(stdlib). - Database:
crystal-db(interface),crystal-pg(Postgres),crystal-mysql,crystal-sqlite3,GraniteORM,Avram(Lucky’s ORM),JenniferORM. - Networking / messaging:
amqp-client.cr(84codes),redis,crystal-kafka,crystal-natsio. - CLI:
option_parser(stdlib),admiral,clim. - Testing: Spec (stdlib, RSpec-style —
describe,it,should/expect). Run withcrystal spec. - Mocking: Crystal lacks runtime metaprogramming for mocks; use protocol-based mocks (
spectator) or hand-rolled. - Docs:
crystal docsgenerates HTML from doc comments (#lines above defs). Style: https://crystal-lang.org/api/. - Notable users: 84codes (CloudAMQP, CloudKarafka — runs production at scale on Crystal), Manas (the originators), Kagi (search backend portions, per Wikipedia), Nikola Motor (vehicle infotainment), NeuralLegion.
Gotchas
- Whole-program compile: Crystal type-infers across your entire program + all dependencies. Adding one shard can blow up compile time. Production builds take seconds-to-minutes; non-incremental.
- Union type inference surprises: a method that returns
Int32 | String | Nilrequires you to narrow before each operation. ForgettingNilis the most common bug. returnin a block: returns from the enclosing method, not the block. Usenextto return a value from the block.- Instance variable type inference: if you only assign
@xonce, the compiler infers its type from that assignment. To allownilinitially, declare@x : Int32?. - Splat surprises:
def f(*args : Int32)with 0 args yieldsTuple()— empty tuple type; type*args : Int32only constrains element type. - Multi-thread mode caveats: many stdlib types are not thread-safe. Audit before flipping
-Dpreview_mt. Channels andAtomicare. - Heap allocation everywhere classes are involved:
classinstances are GC’d; if you need stack/value semantics, usestruct(immutable, value-type, no GC overhead, no inheritance beyond modules). - Shards are Git-based: a force-push or repo deletion breaks builds. Prefer tagged versions in
shard.yml. - No
nilcoalescing operator (no??) — usex || defaultorx.try &.method. Time.nowwas removed in 0.27 — useTime.utcorTime.local.- Macro errors are cryptic: a wrong
{{...}}interpolation yields multi-screen ASTs. Use{% pp ast_node %}to debug. - Boehm GC false retentions are rare but possible;
GC.collectwon’t free things the conservative scan thinks are live. - Compile errors mention every overload: a missing method on a union type lists every variant — overwhelming for big unions.
Citations
- Crystal Language Reference: https://crystal-lang.org/reference/
- Crystal API docs: https://crystal-lang.org/api/
- Install guide: https://crystal-lang.org/install/
- Shards manual: https://crystal-lang.org/reference/the_shards_command/
- Concurrency guide: https://crystal-lang.org/reference/guides/concurrency.html
- C bindings (lib blocks): https://crystal-lang.org/reference/syntax_and_semantics/c_bindings/
- Macros: https://crystal-lang.org/reference/syntax_and_semantics/macros/
- Performance guide: https://crystal-lang.org/reference/guides/performance.html
- Coding style: https://crystal-lang.org/reference/conventions/coding_style.html
- Ameba (linter): https://github.com/crystal-ameba/ameba
- Kemal: https://kemalcr.com/
- Lucky: https://luckyframework.org/
- 84codes (largest user): https://www.84codes.com/
- Wikipedia (history, sponsors, license): https://en.wikipedia.org/wiki/Crystal_(programming_language)