5

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 nil is 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 nil can 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 nil too 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)
worked_examples.md
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)
cheatsheet.md
ruby

Guard clause

return 0 unless user
cheatsheet.md
ruby

Custom exception

class MissingConfigError < StandardError; end
cheatsheet.md
ruby

Fail-fast pattern

raise MissingConfigError, "missing config: #{key}"
cheatsheet.md
ruby

Questions to ask ๐Ÿค”

  • Is absence expected?
  • Does the caller have a sensible fallback?
  • Would returning nil hide 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 ๐Ÿชœ

  1. Read overview.md.
  2. Read shortnote.md and identify the two categories of absence.
  3. Review worked_examples.md.
  4. Use cheatsheet.md while reading example.rb.
  5. Complete the basic exercise about discount decisions.
  6. 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 nil appropriate for profile lookup but not for required config?
  • What bug would be hidden if fetch! quietly returned nil?
  • 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
en/topic_05_control_nil_exceptions/example.rb
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
en/topic_05_control_nil_exceptions/basic_exercise.rb
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
en/topic_05_control_nil_exceptions/adv_exercise.rb
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
en/topic_05_control_nil_exceptions/answer_basic_exercise.rb
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
en/topic_05_control_nil_exceptions/answer_adv_exercise.rb
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
en/topic_05_control_nil_exceptions/tests/topic_05_control_nil_exceptions_spec.rb
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}")"
en/topic_05_control_nil_exceptions/run_topic_tests.sh
Ruby course source

Study Prompts

  1. Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.

  2. Attempt the exercises before opening the answer files, then compare the design choices.