Control Flow, Nil Safety, and Exceptions
Ruby source files are converted directly from the English source tree.
en/topic_05_control_nil_exceptions
Overview
Why this topic matters ๐ก
Real applications need clear behavior for absent data and invalid state. Ruby gives students concise tools such as guard clauses and safe navigation, but the deeper lesson is choosing the right failure mode for the situation.
Learning outcomes ๐ฏ
By the end of this topic, students should be able to:
- explain when
nilis an acceptable result and when an exception is better; - use guard clauses to simplify control flow;
- use safe navigation for optional nested data;
- define and raise a focused custom exception;
- describe the tradeoff between permissive APIs and fail-fast APIs.
Assessment focus โ
Students should be able to defend the difference between "not found" and "misconfigured."
Short Note
Ruby's nil is simple to use, but educationally the important question is not "how do I avoid a crash?" It is "what does absence mean in this domain?"
Some data can be optional:
- a profile email might simply be missing;
- returning
nilcan be appropriate.
Some data is required:
- a configuration entry for a critical integration may be mandatory;
- raising an exception can be the clearer choice.
Ruby beauty in this topic:
- guard clauses keep branches shallow;
- safe navigation handles nested optional data cleanly;
- custom exceptions can express domain meaning.
Ruby caution in this topic:
- returning
niltoo often can hide bugs; - raising everywhere can make APIs exhausting to use.
Reflection prompt:
- What is the semantic difference between "missing user email" and "missing application config"?
Worked Examples
Example 1: Optional user profile data ๐ก
User profile information is often incomplete. Returning nil from a safe lookup is reasonable when the caller can decide what to show next.
user&.dig(:profile, :email)ruby
This is preferable to deep nested conditionals for a simple optional lookup.
Example 2: Required configuration ๐ก
Configuration fetching is a strong contrasting example because missing config is usually a deployment or setup problem, not a routine absence.
Why this is useful:
- students learn to classify errors;
- the custom exception gives the failure a domain-specific name;
- the difference between optional and required data becomes concrete.
Cheatsheet
Safe navigation
user&.dig(:profile, :email)ruby
Guard clause
return 0 unless userruby
Custom exception
class MissingConfigError < StandardError; endruby
Fail-fast pattern
raise MissingConfigError, "missing config: #{key}"ruby
Questions to ask ๐ค
- Is absence expected?
- Does the caller have a sensible fallback?
- Would returning
nilhide a serious bug?
Study Guide
Topic purpose ๐ฏ
Learn to treat absent data and invalid configuration as different design problems. This topic is about judgment as much as syntax.
Study sequence ๐ช
- Read
overview.md. - Read
shortnote.mdand identify the two categories of absence. - Review
worked_examples.md. - Use
cheatsheet.mdwhile readingexample.rb. - Complete the basic exercise about discount decisions.
- Complete the advanced exercise about required configuration.
What to notice ๐
- Guard clauses reduce nesting.
- Safe navigation is useful when absence is normal.
- A custom exception is stronger communication when absence is a system problem.
Reflection questions ๐ค
- Why is returning
nilappropriate for profile lookup but not for required config? - What bug would be hidden if
fetch!quietly returnednil? - How do guard clauses improve readability in small methods?
Source Files and Tests
Ruby source files are converted directly from the English source tree.
# EXAMPLE CODE
# Topic: topic_05_control_nil_exceptions
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.
class ProfileEmail
def extract(user)
user&.dig(:profile, :email)
end
end
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_05_control_nil_exceptions
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_05_control_nil_exceptions_spec.rb under the "basic exercise" examples.
# - Make tests pass without breaking the example/advanced sections.
#
# Expected outcome:
# - You can run this topic tests and see all examples green after implementation.
class DiscountPolicy
def discount_for(user)
return 0 unless user
return 20 if user[:vip]
5
end
end
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_05_control_nil_exceptions
#
# Academic purpose:
# - Distinguish optional data access from mandatory configuration lookup.
# - Practice encoding domain meaning in an exception class rather than raising generic errors.
#
# Real-world use case:
# - Application config often includes API keys, region names, feature flags, or service endpoints.
# - If one of those settings is missing, the system should fail loudly and explain why.
# - This is a useful counterexample to the earlier nil-safe lookup behavior.
#
# Why Ruby is beautiful here:
# - A custom exception is easy to define and gives the failure a clear name.
# - The method can stay short while still communicating a strong failure policy.
# - Guard-style control flow keeps the successful path obvious.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Be able to explain why this case should not silently return `nil`.
# - Use tests in tests/topic_05_control_nil_exceptions_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can justify the fail-fast design.
class MissingConfigError < StandardError; end
class ConfigFetcher
def fetch!(config, key)
return config[key] if config.key?(key)
raise MissingConfigError, "missing config: #{key}"
end
end
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_05_control_nil_exceptions
#
# Solution idea:
# - Use guard clauses to separate cases clearly.
# - No user means no discount.
# - VIP users get the larger discount.
# - Everyone else gets the standard fallback.
class DiscountPolicy
def discount_for(user)
return 0 unless user
return 20 if user[:vip]
5
end
end
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_05_control_nil_exceptions
#
# Solution idea:
# - Missing required configuration is treated as an error, not a normal nil case.
# - A custom exception gives the failure a domain-specific meaning.
class MissingConfigError < StandardError; end
class ConfigFetcher
def fetch!(config, key)
return config[key] if config.key?(key)
raise MissingConfigError, "missing config: #{key}"
end
end
Ruby course source
# This spec is your learning companion for topic_05_control_nil_exceptions.
#
# How to use this file:
# 1) Run tests and observe failures/successes.
# 2) Keep EXAMPLE specs green from the beginning.
# 3) Implement BASIC exercise until BASIC specs pass.
# 4) Implement ADVANCED exercise and pass edge cases.
#
# Expected final result:
# - All examples in this file pass.
# - You understand both the concept and the implementation tradeoffs.
require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"
RSpec.describe "topic_05_control_nil_exceptions" do
describe "EXAMPLE purpose: understand the reference implementation" do
it "extracts profile emails safely" do
e = ProfileEmail.new
expect(e.extract({ profile: { email: "a@b.com" } })).to eq("a@b.com")
expect(e.extract(nil)).to eq(nil)
end
end
describe "BASIC EXERCISE purpose: implement the comparable task" do
it "applies discount rules" do
d = DiscountPolicy.new
expect(d.discount_for(nil)).to eq(0)
expect(d.discount_for(vip: true)).to eq(20)
expect(d.discount_for(vip: false)).to eq(5)
end
end
describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
it "raises custom error for missing config" do
f = ConfigFetcher.new
expect(f.fetch!({ timeout: 30 }, :timeout)).to eq(30)
expect { f.fetch!({}, :timeout) }.to raise_error(MissingConfigError, /timeout/)
end
end
end
Ruby course source
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
Ruby course source
Study Prompts
Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.
Attempt the exercises before opening the answer files, then compare the design choices.