Find + fix software errors faster by getting better at sight reading code.
(via 56 exercises that take only a few minutes each to complete)

Hello There!


Thanks for checking out Bug Hunt: Volume 1.

This free sample includes 10 of 56 exercises that will be in the full guidebook.

If you want some background on how this works and what to expect, start with the introduction.


Rather go straight to the execises? No worries!

All the free samples are accessible via the links in the table below.

SyntaxNullProtocolLogicStateResourceEnvironment
0x000x10--0x40--
0x01--0x31--
0x02-0x22----
0x030x13-----
-0x14----
-------
-------
-------

Starting on March 14, this table will be updated weekly to list what exercises are available for early access readers.
A decent sized batch of those are currently in private beta but are nearly ready to share.


I'm also looking for beta readers to review exercises before they're made generally available.

To gain early access, please put in a preorder and I'll reach out to you as soon as I can.

I'm doing feedback rounds in small batches of 3-5 readers, and sending invites out based on whoever preorders first.


Contact me at gregory@skillstopractice.com or via Bluesky if you've got questions.

Thanks for stopping by. Hope you enjoy solving these puzzles as much as I enjoyed making them.

-greg

Why go bug hunting?

Your own fingertips provide a free lifetime supply of software bugs to find and fix.

So why go out of your way and search for more?

Because deliberate practice1 works. And anything you do often is worth doing well.


The exercises in this guidebook are designed to help you:

  • Practice troubleshooting and repair in a non-urgent setting.

  • Notice patterns that are often missed in just-in-time problem solving mode.

  • Sight read code well enough to build mental models without relying on guesswork.

  • Identify sources of friction in your debugging process that can be overcome with practice.


Each exercise you complete will fine-tune your tactics for finding and fixing software defects.

Working through the set as a whole will give you a sense of what you already know, and where you have room to grow.

And if it does its job well, this guidebook will also reconnect you with the curiosity that is the hallmark of a Beginner's Mind.


Preparing for the hunt


This guidebook is designed for those who have at least some coding experience.

You will also need:

  • A willingness to read code samples and answer questions about them before you run them.

  • A working knowledge of Ruby (but you don't have to be an expert!)

  • A development environment with Ruby 3.4 installed so that you can test out assumptions and explore ideas as you work.

  • A computer or tablet to view this guide on (because reading code on tiny screens is arduous)

  • A place to jot down some notes... could be in an app or on paper, whatever works best.

  • A coffee, a snack, or whatever else gives you sustenance as you get down to work.


Beyond that, you won't need anything more than what you'll find in these pages.

The bugs you'll be hunting


This guidebook covers seven broad categories of software errors, all of which you've likely encountered in your day to day work.


In each section there are 8 exercises to work through that gradually get more complicated.

I'd recommend starting with a fast pass through all of the exercises in order.

Solve the ones that seem easy first, skipping any that might take more than five minutes to solve.

Then revisit any of the more challenging exercises in whatever order you'd like, using the field notes at the back of the guide to get a deeper understanding of what techniques might be helpful for those particular problems.


By now you should have a basic idea of what to expect from this guidebook.
With that out of the way, good luck, and happy hunting!

Syntax Errors :: 0x00 - 0x07

This category of errors can crash your program before it even gets a chance to run.

Often easy enough to spot for an experienced coder but harder to explain the "how" behind the approach used for spotting a missing comma in a haystack.

Syntax Errors :: 0x00

This book begins with the classic first program found in beginner's programming tutorials, and yet true to its own name, there's already a (fairly obvious) bug within it.

puts "Hello World'

What simple fix is needed to prevent this program from crashing?

[ANSWER] To reveal the answer, hover or tap on this box and then click the eye icon.

 The quotes are mismatched (opens with a double quote, closes with a single quote)

 Changing the string to either "Hello World" or 'Hello World' would fix things.

Related Notes :: 0xF000 | 0xF001 | 0xF002

Syntax Errors :: 0x01

Not all syntax errors raise exceptions. Sometimes they lead to unintended behavior instead.

Consider the following code sample:

greeting = "Howdy"

puts '#{greeting} World!'

What will go wrong when this program runs, and how can it be fixed?

[ANSWER] To reveal the answer, hover or tap on this box and then click the eye icon.

 Intended output: Howdy World!

   Actual output: #{greeting} World!

 ...

 This happens because single quoted string literals behave differently
 than double quoted string literals.

 Single quoted strings are meant to represent raw text and so they
 don't support the #{...} interpolation operator.

 ...

 Using double quotes instead of single quotes would fix the issue with
 this code and make the program work as intended.

Related Notes :: 0xF002

Syntax Errors :: 0x02

Leaping straight beyond beginner's tutorials to intermediate coding exercises, you'll find code from Jim Weirich's version of the Gilded Rose Kata listed below.

Your task is not to solve that coding exercise, but instead to figure out:

  1. The one character that was accidentally deleted when copy-pasting the update_quality method into an editor which will crash the program unless fixed.

  2. The reason why the error logs don't seem to call out the true source of the syntax errors, and instead report on issues many lines away from where the problem was introduced.


WARNING: You're about to see some very brittle code.

But you won't need to understand how this code sample works to complete this exercise.

Instead, try to spot (1) and (2) visually, without running the code.


def update_quality(items)
  items.each do |item|
    if item.name != 'Aged Brie' &&
       item.name != 'Backstage passes to a TAFKAL80ETC concert'
      if item.quality > 0
        if item.name != 'Sulfuras, Hand of Ragnaros'
          item.quality -= 1
        end
      end
    else
      if item.quality < 50
        item.quality += 1
        if item.name == 'Backstage passes to a TAFKAL80ETC concert
          if item.sell_in < 11
            if item.quality < 50
              item.quality += 1
            end
          end
          if item.sell_in < 6
            if item.quality < 50
              item.quality += 1
            end
          end
        end
      end
    end
    if item.name != 'Sulfuras, Hand of Ragnaros'
      item.sell_in -= 1
    end
    if item.sell_in < 0
      if item.name != "Aged Brie"
        if item.name != 'Backstage passes to a TAFKAL80ETC concert'
          if item.quality > 0
            if item.name != 'Sulfuras, Hand of Ragnaros'
              item.quality -= 1
            end
          end
        else
          item.quality = item.quality - item.quality
        end
      else
        if item.quality < 50
          item.quality += 1
        end
      end
    end
  end
end

Think you've figured it out? Confirm by revealing the answer block below.

# [ANSWERS] #

 # Take note of the colorization for the following code sample.

  if item.name == 'Backstage passes to a TAFKAL80ETC concert
    if item.sell_in < 11
      if item.quality < 50
        item.quality += 1
       end
     end
     if item.sell_in < 6
        if item.quality < 50
          item.quality += 1
        end
      end
    end
  end
end
if item.name != 'Sulfuras


 # What you'll find is that due to the missing quote on the string in the first
 # line of this snippet, the entire chunk of code that follows it is parsed
 # as if it were part of the string and *not* as code.

 # And because it interprets the single quote before Sulfaras as the end of a
 # a string rather than the beginning, the word Sulfaras itself is parsed
 # as a constant.
 #
 # Then in addition to that issue being detected, Ruby 3.4 will also inform you
 # that the condition on the first line listed above never had a matching `end`
 # keyword. This happens because what it actually "sees" is one big string
 # of raw text and not code.
 #
 # So even though Ruby's error messages are helpful here if you can reason about
 # how the code is being parsed, the easier and more immediate way to spot this
 # particular type of error is just to notice that the colorization is off,
 # and then look at where that starts happening and try to see if there's an
 # obvious issue right around there.

Related Notes :: 0xF002

Syntax Errors :: 0x03

At first glance, nothing looks weird about how the following program is colorized.

But continuing on a theme, a botched auto-completion on a single line of its code has broken things in a way that will cause it to crash before it ever gets a chance to run:

class Bag
  def initialize
    @items = []
  end

  def add_item(item)
    @items.push(item)
  else

  def take_item
    @items.shuffle!
    @items.pop
  end
end

bag = Bag.new

bag.add_item("cranberries")
bag.add_item("stuffing")
bag.add_item("turkey")

p bag.take_item

(1) Where is the broken line of code and how would you fix it?

# [ANSWER 1] #

   The `add_item` method is missing an `end` keyword, which was accidentally
   written as `else` instead.

(2) If this code is run as-is, Ruby will report both the problematic line that needs fixing, as well as another syntax error elsewhere in the code. What causes that to happen?

# [ANSWER 2] #

     Ruby does not stop trying to parse the method definition for `add_item`
     when it hits the line with the `else` keyword on it, but instead
     keeps going as if the `take_item` method definition had been nested inside it.

     This makes it so that the end that was meant to close the `Bag` class
     definition is treated as if it is the end of the `add_item` method instead,
     and all the code that follows is still inside the `Bag` class definition.

     So in addition to the error about the misplaced `else` keyword, you end up
     getting an error that the parser reached the end of the file without finding
     an `end` for the `Bag` class definition.

Hint: Understanding what the Bag object is and how it is used in this example program *won't* help you answer these questions. However, if you focus on reading the code one color at a time rather than one line at a time, the bug will likely jump straight out at you.

Related Notes :: 0xF003

0x04 - 0x07

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Null Errors

The source of the dreaded yet omni-present NoMethodError for nil:NilClass, along with a wide range of other unwelcome system behaviors.

Finding out where a method was called on something that wasn't supposed to be nil is easy... figuring out how and why the thing you thought was something ended up being nothing is often more involved.

Null Errors :: 0x10

Back to the wonderful world of "Exactly right except for a single keystroke" we go.

If it was written correctly, this program would output a random number between 5 and 500. But due to a typo, it'll fail to behave as expected.

@running_total = 0

5.times { @running_total += rand(1..100) }

p @running_t0tal

(1) If the code was run as-is, what will the output be?

# [ANSWER 1] #

 nil

(2) If the same typo was present but the running total was stored in a local variable instead of an instance variable, would you get the same result, or would the program behavior change?

# [ANSWER 2] #

The program would crash (raising a NameError) with the message:
"undefined local variable or method 'running_t0tal' for main"

The field notes for this exercise are not included in the free sample, but will be provided in the paid version of this guidebook.

You can preorder now to be among the first to gain access when it is released.

0x11 - 0x12

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Null Errors :: 0x13

The more layered a codebase is, the bigger the gap tends to be between where a coding error is introduced, and where the problematic behavior bubbles up to the surface.

To see what that can look like in practice, let's walk through an example where you try to make sense of a code sample by observing its behavior from the outside first, and then peel back one layer at a time until you reach the source of the problem with its implementation.


Consider the following code sample which checks whether a particular Person can rent a car:

renter = Person.new(Date.new(2001, 3, 14))

p can_rent_car?(renter)

Even without knowing the specific rules for who is allowed to rent a car and who isn't, Ruby conventions imply that can_rent_car? will return a true or false value.

Upon running the code, you discover that it does not behave as expected.

Instead, it raises an exception with the following error message:

% ruby rental.rb
rental.rb:15:in 'Object#can_rent_car?':
Cannot verify renter eligibility. Age unknown. (RuntimeError)
	from rental.rb:23:in '<main>'

This prompts you to take a look at the can_rent_car? method to see what's going in within it:

def can_rent_car?(person)
  if person.age == :unknown
    raise "Cannot verify renter eligibility. Age unknown."
  end

  person.age >= 21
end

Aside from it being somewhat of a questionable practice to raise an exception from a conditional method of this sort in Ruby, there's nothing obviously wrong with this code.

It effectively tells you that a Person is expected to be 21 or older in order to rent a car, and that this method ought to only ever be called with a Person that has a known age.

But in the earlier code sample, the Person object was initialized with a Date that was presumably their birthdate (2001-03-14), so... there has to be something wrong elsewhere in the codebase.

So let's pull up the Person class definition and take a closer look:

require "date"

class Person
  def initialize(birth_date)
    @brith_date = birth_date
  end

  def age
    @birth_date ? ((Date.today - @birth_date) / 365.0) : :unknown
  end
end

Q1: What tiny mistake was made in the definition of the Person class which is causing age to incorrectly report :unknown, even when a birth date is provided?

# [ANSWER 1] #

 There is a typo in the `initialize` method:
    @brith_date should be @birth_date

Q2: Suppose that you could take for granted that a birth date is a required attribute of a Person object, and that when it is unknown, it must be explictly set to :unknown.

How could you revise the code to be less prone to this particular type of coding error?

# [ANSWER 2] #

 There are a number of different possible ways to solve this problem.

 One potential solution would be to make use of pattern matching:

    class Person
      def initialize(birth_date)
        @birth_date = birth_date

        @birth_date => Date | :unknown
      end

      def age
        return :unknown if @birth_date == :unknown

        (Date.today - @birth_date) / 365.0
      end
    end

 Any approach that would cause the exception to be raised from the
 Person constructor where it actually occured would prevent this type
 of mistake from going unnoticed and bubbling up elsewhere in otherwise
 correctly written code.

The field notes for this exercise are not included in the free sample, but will be provided in the paid version of this guidebook.

You can preorder now to be among the first to gain access when it is released.

Null Errors :: 0x14

Suppose you are helping a friend who is working on an exercise for a beginner's coding course.

The exercise involves creating a sample program to demonstrate some basic error handling techniques. But their implementation is not quite working as expected, and they've asked you to help you figure out what's going wrong.


The program is supposed to simulate syncing an inbox and displaying its new messages:

inbox = Inbox.new("gregory@skillstopractice.com")

inbox.sync_messages
inbox.each { |e| p e }

Most of the time, it will run successfully and display up to 10 sample messages with randomized subjects and bodies to the console:

{:subject=>"B", :body=>"Sweet"}
{:subject=>"A", :body=>"Sweet"}
{:subject=>"C", :body=>"Nice"}
{:subject=>"B", :body=>"Nice"}
{:subject=>"B", :body=>"Awesome"}
{:subject=>"B", :body=>"Nice"}
{:subject=>"B", :body=>"Awesome"}
{:subject=>"A", :body=>"Awesome"}
{:subject=>"A", :body=>"Sweet"}

But the sample program also needs to simulate a scenario where the mail server is down. When that happens, the program should print the following error log rather than displaying the messages:

Problem reaching the mail server, please try again.

This will have a 1 in 20 chance of occurring for each fetched message.


Your friend has their example program almost working, but has been scratching their head because in addition to the "Problem reaching the mail server" message being displayed, there's a stack trace showing the backtrace for a NoMethodError exception that they didn't expect:

Problem reaching the mail server, please try again
demo.rb:[...]:in `[...]': undefined method `...` for
nil:NilClass (NoMethodError)
	from demo.rb:...:in `<main>'
Note: The line numbers and method name have been intentionally redacted. Figuring out where they're coming from is part of the exercise, as you'll see below.

Simply seeing the nil:NilClass (NoMethodError) part of this trace makes you immediately aware that there's a null value somewhere in this code sample that there shouldn't be.

Their complete code listing is shown below. Skim through it quickly first, then read the questions that follow it, then read the code again as you try to answer them:

class Inbox
  def initialize(address)
    @address = address
  end

  def sync_messages
    @messages = MailServer.fetch_mail_for(@address)
  rescue
    STDERR.puts "Problem reaching the mail server, please try again"
  end

  def each
    @messages.each { |e| yield e }
  end
end

class MailServer
  def self.fetch_mail_for(address)
    rand(1..10).times.map { fetch_fake_message }
  end

  def self.fetch_fake_message
    raise if rand(1..20) == 1

    { :subject => ["A", "B", "C"].sample,
      :body => ["Nice", "Sweet", "Awesome"].sample }
  end
end

#--------------------------------------------------------------------

inbox = Inbox.new("gregory@skillstopractice.com")

inbox.sync_messages
inbox.each { |e| p e }

If you are able to answer the following questions correctly, you should be able to help your friend find and fix the bug in no time:

(1) What line causes the NoMethodError exception to be raised?

# [ANSWER 1] #

 # in Inbox#each definition

 @messages.each { |e| yield e }

(2) Which method call actually triggered the NoMethodError to be raised?
(in other words, what name would sappear in the undefined method '...' part of the stack trace?)

# [ANSWER 2] #

The @messages.each { ... } call raises a NoMethodError because @messages is nil.

demo.rb:[..]:in `[..]': undefined method `each' for

(3) What incorrect assumption was made that caused this error to be raised?

# [ANSWER 3] #

 The Inbox#each method assumes that @messages will always be present
 (even if it might be empty)

 However, when MailServer.fetch_mail_for raises an exception, this
 prevents @messages from ever being set in Inbox#sync_messages.

 If @messages were set to an empty array in Inbox#initialize, this
 problem would not occur.

The field notes for this exercise are not included in the free sample, but will be provided in the paid version of this guidebook.

You can preorder now to be among the first to gain access when it is released.

0x15 - 0x17

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Protocol Errors

Every time you interact with an object by calling methods on it, you are making assumptions about how it will respond to the messages you send.

When the actual system behavior does not match your expectations, things tend to break, often in unpredictable ways.

0x20 - 0x21

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Protocol Errors :: 0x22

You've built a "Treasure Map" example program to teach a friend basic coding concepts. It's simple enough... it displays a text based grid for a map of a certain size, and then picks a random place where "X marks the spot" that the treasure is at.

- - - - -
- - - - -
- - - - -
- X - - -
- - - - -

The first few times you run the program, it works fine. Then on one of the test runs, you get weird output that isn't what you'd expect, even though the program doesn't crash.

- - - - -
- - - - -
- - - - -
- - - - - X
- - - - -

You run it a few more times just for good measure, and then things get even worse... it appears that there's a Null Error lurking in the code as well, and the program does crash when it hits that issue, without even printing out the map.

map.rb:13:in 'Map#mark': undefined method '[]=' for nil (NoMethodError)

    @grid[point.y][point.x] = "X"
                  ^^^^^^^^^^^
	from map.rb:25:in '<main>'

Your bug hunting powers have been getting stronger and stronger lately, so you'll undoubtedly be able to figure out what's going wrong here.

All it takes is some careful code reading.

Review the code sample below to identify the root cause for why the program intermittently fails, and then describe how you'd fix it.

Point = Data.define(:x, :y)

class Map
  def initialize(size)
    @size = size

    @grid = @size.times.map { Array.new(@size, "-") }
  end

  attr_reader :size

  def mark(point)
    @grid[point.y][point.x] = "X"
  end

  def to_s
    @grid.map { |row| row.join(" ") }.join("\n")
  end
end

## build a 5x5 map
map = Map.new(5)

## Hide a treasure in a random location on the map
map.mark(Point[rand(1..map.size), rand(1..map.size)])

puts map

The answers and field notes for this exercise are not included in the free sample, but will be provided in the paid version of this guidebook.

You can preorder now to be among the first to gain access when it is released.

0x23 - 0x27

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Logic Errors

A newly built system's rules often are few and optimistic in nature, and so spotting mistakes within them is easy enough.

But those rules inevitably evolve as a system grows. And because their edge cases tend to multiply, subtle flaws accumulate that are only obvious to those with enough domain knowledge to spot them.

These errors often don't cause systems to crash nor do they necessarily cause old tests (which are based on simpler use cases) to fail, and that makes them especially challenging to find and fix.

0x30

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Logic Errors :: 0x31

You're reviewing a pull request from another developer who is building a replacement online store for your company.

You look through the diff quickly and see that the feature they're currently working on is to add discounts to a shopping cart object.

@@ -2,7 +2,8 @@ class Cart
   SALES_TAX = 0.05

   def initialize
-    @items = []
+    @items    = []
+    @discount = 0
   end

   def <<(item)
@@ -13,10 +14,14 @@ class Cart
     @items.sum(&:price)
   end

+  def apply_discount(amount)
+    @discount = amount
+  end
+
   def total
     tax_adjustment = (1 + SALES_TAX)

-    (subtotal * tax_adjustment).round(2)
+    ((subtotal * tax_adjustment) - @discount).round(2)
   end
 end

@@ -26,6 +31,8 @@ cart = Cart.new
 cart << LineItem["An exciting shirt", 21]
 cart << LineItem["An equally exciting pair of socks", 5]

-if cart.total != 27.30
+cart.apply_discount(5.00)
+
+if cart.total != 22.30
   fail "Something went wrong! Please check your calculations."
 end

You notice there is a test at the bottom of the diff which has been modified to reflect the new feature in use, but something about it looks off to you.

So you investigate a little further, just to be sure.

You pull up the old online store app and create two products with the same names and prices shown in the test, and then add them to the cart. And then you create a $5 off coupon and apply it in the cart.

After doing that, you see that the total listed in the old app is $22.05 and not $22.30 as the test code in this new change indicated. The old app has been online for a decade, and you can safely assume that its math is correct.

(1) What did the developer of the new app get wrong about the logic for how discounts should be applied?

(2) Suppose that instead of the values used in this example, you had two items, one costing $75 and the other costing $25, the sales tax was 10%, and the discount was $10. What would be the correct total then?


The answers and field notes for this exercise are not included in the free sample, but will be provided in the paid version of this guidebook.

You can preorder now to be among the first to gain access when it is released.

0x32 - 0x37

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

State Errors

The more complex a workflow gets, the more likely that it'll be that you cannot simply read code in a straight line from beginning to end and check to make sure it is correct at each step along the way.

Instead, logic will branch and behaviors will vary based on the state of each object in the system. Simply checking every possible configuration for each object is often unrealistic due to an explosion of combinatorial complexity.

This is where building a mental model for how a system should work becomes key in figuring out what's going wrong with it and why.

State Errors :: 0x40

Once again, a friend is preparing example programs for a blog post explaining how to handle error conditions in Ruby.

They send along a sample class for you to take a look at. It seems simple enough, as it's a variation on themes they've covered before.

class Bag
  def initialize(size)
    @size  = size
    @items = []
  end

  def <<(item)
    raise "Bag is full!" unless @items.length < @size

    @items << item
  end

  def count
    @items.count
  end
end

They also share a sample main program meant to demonstrate how the code works, which also seems fairly straightforward:

bag = Bag.new(3)

bag << "Apples"
bag << "Bananas"
bag << "Oranges"

p bag.count #=> 3

# this will raise an exception because the bag is full
bag << "Elephants"

Before you get a chance to properly review, they send a followup message wondering if they'd be better off simply giving the reader something concise to run which adds all the items at once, skipping the printout of the bag.count:

Bag.new(3) << "Apples" << "Bananas" << "Oranges" << "Elephants"

You give that some thought, but then realize upon closer reading of the Bag class definition that this isn't going to work as your friend expects it to.

In fact, it won't raise an error at all!

(1) What causes this code to bypass the raise "Bag is full" guard, and how can it be fixed?


The answers and field notes for this exercise are not included in the free sample, but will be provided in the paid version of this guidebook.

You can preorder now to be among the first to gain access when it is released.

0x41 - 0x47

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Resource Errors

Every program interacts with the outside world, whether via the console, the filesystem, a database, the internet, or any number of other external sources.

Any time we rely upon data that exists outside of our program, we run the risk of encountering errors that arise from interacting with these external systems... which we must handle appropriately to avoid letting those failures cascade into our own programs.

0x50 - 0x57

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Environment Errors

Finally, there are many potential sources of chaos that are seemingly invisible until they're not, which has to do with how, where, and when your program is run rather than what its purpose is.

This might manifest as an error you've never seen before because someone's DNS server on the other side of the world was misconfigured, or a program that simply won't boot up at all without recompiling Ruby because somehow your operating system broke your OpenSSL setup for the 37th time. Or because you hopped on a plane and can no longer access a service that was region locked.

Some of the most frustrating and hard to debug problems lurk in this category. But it's what makes our work an engineering discipline, and is just part of the job.

0x60 - 0x67

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

Field Notes : Syntax Errors


Mismatched or missing delimiters are the most common syntax errors [0xF000]

Syntax error stack traces tell you where the parser failed, not where the mistake was made. [0xF001]

Parsers have no understanding of what your intentions are, they just follow strict grammar rules to transform code into structures and expressions. [0xF002]

Because Ruby is such an expressive language, the rules for parsing it are often more complex than you'd think. [0xF003]

Mismatched or missing delimiters are the most common syntax errors [0xF000]


Source code is just text in a file. It only becomes a runnable program after the parser transforms that text into structures and expressions that Ruby understands.

A syntax error happens when the parser (for whatever reason) cannot complete its job. Among the most simple ways for that to happen is when it can't figure out where one structure or expression ends, and another one begins.

This almost always happens by accident. A single missed quote, bracket, brace, slash, parenthesis, or end keyword can cause the parser to run far beyond the intended end of a structure, stopping only when it happens to match the same symbol somewhere else in the code that doesn't make logical sense, or failing that, at the end of the file.

Related Exercises :: 0x00

Syntax error stack traces tell you where the parser failed, not where the mistake was made. [0xF001]


The challenging thing about this category of error is that because of how the parser actually works, it can only tell you where a particular structure started, and where it gave up trying to parse it.

So the real mistake will end up being somewhere in between those two points. But the error message will almost never tell you exactly where that is.

With that in mind, you can usually find the source of the error by starting at the beginning of the structure and visually scanning the code until you see where the mistake was made.

Related Exercises :: 0x00

Parsers have no understanding of what your intentions are, they just follow strict grammar rules to transform code into structures and expressions. [0xF002]



SPOILER FOR [ 0x00 ] - CLICK TO REVEAL

Note how the error message for problem 0x00 doesn't tell you that the ' at the end of the line is *wrong*, instead what it tells you is that the string literal (which it started to try to parse as soon as it saw the " symbol) was never terminated:

    
    examples/hello.rb:1: syntax error found (SyntaxError)
    > 1 | puts "Hello World'
        |                   ^ unterminated string meets end of file
    
  

This is because it's technically just fine to have a ' inside of a double quoted string,
for example "It's a wonderful world".

And so the parser doesn't try to make sense of what the ' means at all in this code, it just looks for a " to end the string and never finds it, so it hits the end of the file.

SPOILER FOR [ 0x01 ] and [ 0x02 ] - CLICK TO REVEAL
Having a mental model for how the parser will apply a particular rule is helpful.

In the case of single quoted string literals, it's "keep scanning forward in the file until you hit either a backslash or a closing single quote, treating everything else as raw unprocessed text.

In problem [0x01] the intention is clearly to make use of string interpolation.

But in a single quoted string, nothing is treated as a special character except the backslash which is used for escaping purposes (e.g. 'It\'s a wonderful World')

So the string parses fine because it doesn't know or care what's in it, even though it leads to the wrong output by not actually doing the interpolation operation.

This also explains why the parser actually does not immediately fail to complete the string with the missing ' in problem [0x02], but instead, completes it in the wrong place. It happily includes a bunch of lines of code as raw text in the string, only stopping once it hits an unrelated ' farther down the file.

Related Exercises :: 0x00 | 0x01 | 0x02

Because Ruby is such an expressive language, the rules for parsing it are often more complex than you'd think. [0xF003]


You will often have a clear sense of how the code you meant to write works.

The problem with syntax errors is that tiny mistakes can totally throw the parser off in ways that you might never expect unless you have a very deep knowledge of Ruby's more obscure features.

Problem [0x03] is designed to illustrate how that plays out in practice.

It triggers three Ruby features by accident due to the typo:

  • The ability to have an else clause after a rescue clause in a method definition.
  • The ability to define methods within method definitions.
  • The ability to write any Ruby expression directly inside a class definition.

Optional additional details follow inside the spoiler section below.

SPOILER FOR [ 0x03 ] - CLICK TO REVEAL

In isolation, the accidental use of else instead of end in add_item method is easy to spot:

def add_item(item)
  @items.push(item)
else

But if it's invalid code, why does the colorization seem to work as normal? Because in fact, else is a valid keyword to use within a method definition, it just needs to follow a rescue segment.

def coin_flip
  [:heads, :tails].sample == :heads && raise
rescue
  "The coin landed on heads"
else
  "The coin landed on tails"
end

How often is this feature used? To be honest, I can't remember the last time I've seen it in the wild. So I'd guess not often at all.

But it's still a valid language construct, and so it's part of what the parser will look for when trying to break a method definition down into its parts.

Things get even more complicated when you throw into the mix the bit of trivia that method definitions technically can include other method definitions, which do not get applied until they've been executed at runtime:

def coin_flip
  [:heads, :tails].sample == :heads && raise
rescue
  "The coin landed on heads"
else
  def lucky_method_call
    puts "You got lucky!"
  end
  "The coin landed on tails"
end

puts coin_flip

puts lucky_method_call

This code will either print "The coin landed on heads" and then crash because lucky_method_call was never defined, or it'll print "You got lucky!" followed by "The coin landed on tails."

Either way, it would most likely make others reading your code scratch their heads, so it is not recommended.

However, because it is possible to write this program, the parser must allow for it in its ruleset.

Just for good measure, throw in one more Ruby feature that is sometimes not obvious how it works even though it comes up often... the ability to write arbitrary expressions anywhere within a class definition.

class Flipper
  include SomeMixin  #1

  p rand(1..100)     #2
end

When written in macro-like style as shown in #1, it isn't obvious from the outside in that include is actually a method call, not some special keyword.

In fact, it's just an ordinary Ruby expression, so writing something like #2 right in the middle of the class definition is also totally acceptable.

I've used a contrived example here to not get too bogged down in details, but this feature is useful at times -- it's just not one that you'd typically reach for daily, so it might not be front of mind while reading code until something forces you to focus on it unexpectedly.

Taking all three of these Ruby features into account, and changing the indentation of the file to be closer to what the parser "sees" in [0x03], a different shape emerges:

class Bag
  # ...

  def add_item(item)
    # ...
  else
    def take_item
      # ...
    end
  end

  bag = Bag.new
  # ...

Looked at this way, it becomes logical why Ruby crashes with error messages about the else keyword not being valid without a rescue, and the missing end for the Bag class rather than for the add_item method.

The more you understand the range of features Ruby supports, the easier it will be to cut through the noise when you see feedback from the parser that doesn't match your intentions.

But simply being aware of the fact that the scope of possible valid programs is far broader than the scope of reasonable and useful programs will go a long way in breaking your mind's tendency to focus on what it meant to write rather than how code actually works.

Related Exercises :: 0x03


Notes on problems 0x04 - 0x07 will be provided in the paid version of this guidebook.

You can preorder now to be among the first to gain access when it is released.


(!!) Field Notes : Null Errors

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

(!!) Field Notes : Protocol Errors

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

(!!) Field Notes : Logic Errors

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

(!!) Field Notes : State Errors

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

(!!) Field Notes : Resource Errors

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

(!!) Field Notes : Environment Errors

This material is not available in the free preview, but will be in the full guidebook.
You can preorder now to be the first to gain access when these exercises are available.

I would like to thank Jessica Battle and Milo Quigley for being the first to playtest and provide feedback on this guidebook.

Additional credits:

CHANGELOG

0.1.0 (Free Preview) - 2025.03.04


Initial preview release! 🎉

Includes the 10 exercises in the free sample.

SyntaxNullProtocolLogicStateResourceEnvironment
0x000x10--0x40--
0x01--0x31--
0x02-0x22----
0x030x13-----
-0x14----
-------
-------
-------

AI Usage Notice

Generative AI has not been used to write any prose or code samples you'll find in this guidebook.

The images of the bug on the front cover, and the lock images were generated using ChatGPT and then substantially edited by the author.

They will be replaced with original artwork produced by a human artist without the use of AI as soon as this book sells enough copies to have a budget to do so.

Unless otherwise noted, no other images found within this guidebook have been AI generated.

This is not a statement in general against the use of AI, but it does reflect the author's personal preferences when it comes to creative works.

COPYRIGHT NOTICE


Bug Hunt : Volume 1 skillstopractice.com

Copyright © 2025 Gregory Brown

All rights reserved.