Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
Crystal is a programming language with Ruby-like syntax and static type checking. It aims to have the elegance and productivity of Ruby combined with the speed, efficiency, and type safety of a compiled language.
Crystal, despite being a modern language with many safeguards, has several common anti-patterns that can lead to performance issues, maintainability problems, and bugs. Here are the most important anti-patterns to avoid when writing Crystal code.
# Anti-pattern: Using generic Object type
def process(data : Array(Object))
data.each do |item|
if item.is_a?(String)
puts item.upcase
elsif item.is_a?(Int32)
puts item + 1
end
end
end
# Better approach: Use union types
def process(data : Array(String | Int32))
data.each do |item|
case item
when String
puts item.upcase
when Int32
puts item + 1
end
end
end
# Even better: Use generics with constraints
def process(data : Array(T)) forall T
data.each do |item|
puts item
end
end
Leverage Crystal’s type system instead of using generic Object types. Use union types, generics, and type restrictions to make your code more type-safe and efficient. This helps catch errors at compile time rather than runtime.
# Anti-pattern: Using exceptions for control flow
def find_user(id)
begin
user = User.find(id)
return user
rescue
return nil
end
end
# Better approach: Use nil or Result types
def find_user(id) : User?
User.find?(id) # Returns nil if not found
end
# Alternative: Use a Result type
record Success(T), value : T
record Failure, error : String
alias Result(T) = Success(T) | Failure
def find_user(id) : Result(User)
user = User.find?(id)
if user
Success.new(user)
else
Failure.new("User not found")
end
end
Avoid using exceptions for normal control flow. Exceptions should be reserved for exceptional conditions. Instead, use nil or Result types to represent the possibility of failure in a type-safe way.
# Anti-pattern: Unsafe nil handling
def get_username(user_id)
user = User.find(user_id) # Might raise if not found
return user.username # Might raise if username is nil
end
# Better approach: Use safe navigation operator
def get_username(user_id)
user = User.find?(user_id)
return user.try &.username
end
# Even better: Use explicit nil handling
def get_username(user_id) : String?
user = User.find?(user_id)
return nil unless user
user.username
end
Use proper nil handling techniques like the safe navigation operator (&.
), nil checks, or the try
method. This makes your code more robust and prevents nil-related runtime errors.
# Anti-pattern: Using mutable global variables
$config = {"api_key" => "secret"}
def update_config(key, value)
$config[key] = value
end
def use_config
puts $config["api_key"]
end
# Better approach: Use class variables with controlled access
class Config
@@instance = {"api_key" => "secret"}
def self.get(key)
@@instance[key]?
end
def self.set(key, value)
@@instance[key] = value
end
end
# Usage
Config.set("api_key", "new_secret")
puts Config.get("api_key")
Avoid using mutable global state. Global variables make code harder to reason about, test, and maintain. Instead, use dependency injection, class variables with controlled access, or dedicated configuration objects.
# Anti-pattern: Using positional arguments for complex methods
def create_user(name, email, age, admin, active)
# Create user with the given parameters
end
# Usage - unclear what each argument means
create_user("John Doe", "john@example.com", 30, true, false)
# Better approach: Use named arguments
def create_user(name : String, email : String, age : Int32, admin : Bool = false, active : Bool = true)
# Create user with the given parameters
end
# Usage - much clearer
create_user(
name: "John Doe",
email: "john@example.com",
age: 30,
admin: true,
active: false
)
Use named arguments for methods with many parameters or boolean flags. This makes your code more readable and less prone to errors from mixing up argument order.
# Anti-pattern: Inefficient string concatenation
def build_report(items)
report = ""
items.each do |item|
report += "Item: #{item.name}, Price: #{item.price}\n"
end
report
end
# Better approach: Use string interpolation or String.build
def build_report(items)
String.build do |str|
items.each do |item|
str << "Item: #{item.name}, Price: #{item.price}\n"
end
end
end
# Alternative: Use array join
def build_report(items)
items.map { |item| "Item: #{item.name}, Price: #{item.price}" }.join("\n")
end
Avoid inefficient string concatenation in loops. Instead, use String.build
, array joining, or string interpolation for better performance and readability.
# Anti-pattern: Misusing fibers without synchronization
def process_data(items)
results = [] of String
items.each do |item|
spawn do
result = process_item(item)
results << result # Unsafe concurrent access
end
end
Fiber.yield
results
end
# Better approach: Use channels for communication
def process_data(items)
channel = Channel(String).new
items.each do |item|
spawn do
result = process_item(item)
channel.send(result)
end
end
results = [] of String
items.size.times do
results << channel.receive
end
results
end
Use Crystal’s concurrency features properly. When using fibers with spawn
, ensure proper synchronization for shared resources using channels, mutexes, or other synchronization primitives.
# Anti-pattern: Ignoring errors
def save_user(user)
user.save
puts "User saved"
end
# Better approach: Handle errors explicitly
def save_user(user)
begin
user.save
puts "User saved"
rescue ex : ValidationError
puts "Validation error: #{ex.message}"
false
rescue ex
puts "Unexpected error: #{ex.message}"
false
else
true
end
end
Implement proper error handling in your code. Don’t ignore exceptions; catch and handle them appropriately. Use specific rescue clauses for different error types to provide better error messages and recovery strategies.
# Anti-pattern: Reinventing the wheel
def parse_json(json_string)
# Custom JSON parsing logic
end
# Better approach: Use the standard library
require "json"
def parse_json(json_string)
JSON.parse(json_string)
end
Leverage Crystal’s rich standard library instead of reinventing the wheel. The standard library provides efficient, well-tested implementations for common tasks like JSON parsing, HTTP requests, file I/O, and more.
# Anti-pattern: Missing type annotations where helpful
def process_data(data)
# What type is data? What does this return?
data.map { |item| item.to_s.upcase }
end
# Better approach: Add type annotations for clarity
def process_data(data : Array(Int32)) : Array(String)
data.map { |item| item.to_s.upcase }
end
Add type annotations to method signatures when it helps with clarity, especially for public APIs. While Crystal can often infer types, explicit annotations make your code more self-documenting and can catch type errors earlier.
# Anti-pattern: Duplicated code without abstractions
def process_users
users = User.all
users.each do |user|
# Complex processing logic
end
end
def process_orders
orders = Order.all
orders.each do |order|
# Similar complex processing logic
end
end
# Better approach: Use abstractions
module Processable
abstract def process
end
class User
include Processable
def process
# User-specific processing
end
end
class Order
include Processable
def process
# Order-specific processing
end
end
def process_items(items : Array(Processable))
items.each &.process
end
Use proper abstractions like modules, inheritance, and generics to avoid code duplication. This makes your code more maintainable and extensible.
# Anti-pattern: Excessive mutability
class User
property name : String
property email : String
def initialize(@name, @email)
end
def update_email(new_email)
@email = new_email
end
end
# Better approach: Use immutability where appropriate
class User
getter name : String
getter email : String
def initialize(@name, @email)
end
def with_email(new_email) : User
User.new(name, new_email)
end
end
Consider using immutability for your data structures when appropriate. Immutable objects are easier to reason about, especially in concurrent contexts. Use methods that return new instances instead of modifying existing ones.
# Anti-pattern: Using strings for fixed sets of values
def process_status(status : String)
case status
when "pending"
# Handle pending
when "processing"
# Handle processing
when "completed"
# Handle completed
else
raise "Invalid status: #{status}"
end
end
# Better approach: Use enums
enum Status
Pending
Processing
Completed
end
def process_status(status : Status)
case status
when Status::Pending
# Handle pending
when Status::Processing
# Handle processing
when Status::Completed
# Handle completed
end
end
Use enums for fixed sets of related values instead of strings or integers. Enums provide type safety, better documentation, and prevent invalid values.
# Anti-pattern: Overusing macros for simple cases
macro define_getter(name, value)
def {{name.id}}
{{value}}
end
end
define_getter :api_url, "https://api.example.com"
# Better approach: Use simpler constructs when possible
API_URL = "https://api.example.com"
def api_url
API_URL
end
# Appropriate use of macros for complex metaprogramming
macro define_json_mappings(properties)
{% for key, type in properties %}
property {{key.id}} : {{type.id}}
{% end %}
def self.from_json(json : String)
data = JSON.parse(json)
instance = new
{% for key, type in properties %}
instance.{{key.id}} = data[{{key.stringify}}].as_s
{% end %}
instance
end
end
Use macros judiciously. While Crystal’s macro system is powerful, overusing macros can make code harder to understand and debug. Use simpler constructs like methods, constants, or classes when they suffice, and reserve macros for cases where metaprogramming is truly needed.
# Anti-pattern: Insufficient testing
class Calculator
def add(a, b)
a + b
end
end
# Better approach: Write proper tests
require "spec"
describe Calculator do
describe "#add" do
it "adds two positive numbers" do
calc = Calculator.new
calc.add(2, 3).should eq(5)
end
it "adds a positive and a negative number" do
calc = Calculator.new
calc.add(2, -3).should eq(-1)
end
it "adds two negative numbers" do
calc = Calculator.new
calc.add(-2, -3).should eq(-5)
end
end
end
Write comprehensive tests for your code. Crystal’s spec framework makes it easy to write and run tests. Test different scenarios, edge cases, and error conditions to ensure your code works correctly in all situations.
# Anti-pattern: Insufficient documentation
class User
def initialize(@name, @email)
end
def valid?
@email.includes?("@") && @name.size > 0
end
end
# Better approach: Add proper documentation
# A class representing a user in the system.
class User
# Creates a new user with the given name and email.
#
# ## Parameters
# * name : The user's full name
# * email : The user's email address
def initialize(@name : String, @email : String)
end
# Checks if the user has valid information.
#
# Returns true if the email contains '@' and the name is not empty.
def valid? : Bool
@email.includes?("@") && @name.size > 0
end
end
Document your code properly, especially public APIs. Include descriptions of classes, methods, parameters, return values, and any exceptions that might be raised. Good documentation makes your code more accessible to others and to your future self.
# Anti-pattern: Not closing resources properly
def read_file(path)
file = File.open(path)
content = file.gets_to_end
# file is never closed
content
end
# Better approach: Use blocks for automatic resource management
def read_file(path)
File.open(path) do |file|
file.gets_to_end
end
end
Use proper resource management techniques. When working with resources like files, database connections, or network sockets, use blocks that automatically handle closing the resource, or ensure you close them manually in a begin
/ensure
block.