(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.
Syntax | Null | Protocol | Logic | State | Resource | Environment |
---|---|---|---|---|---|---|
0x00 | 0x10 | - | - | 0x40 | - | - |
0x01 | - | - | 0x31 | - | - | |
0x02 | - | 0x22 | - | - | - | - |
0x03 | 0x13 | - | - | - | - | - |
- | 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:
-
The one character that was accidentally deleted when copy-pasting the
update_quality
method into an editor which will crash the program unless fixed. -
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.
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.
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.
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"
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.
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>'
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.
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
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?
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?
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]
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
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:
- The book itself is generated using mdBook
- The font used on the cover is called Hacked, and was made by David Libeau.
CHANGELOG
0.1.0 (Free Preview) - 2025.03.04
Initial preview release! 🎉
Includes the 10 exercises in the free sample.
Syntax | Null | Protocol | Logic | State | Resource | Environment |
---|---|---|---|---|---|---|
0x00 | 0x10 | - | - | 0x40 | - | - |
0x01 | - | - | 0x31 | - | - | |
0x02 | - | 0x22 | - | - | - | - |
0x03 | 0x13 | - | - | - | - | - |
- | 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.