Ruby Exception Handling

By default, Ruby programs run one task at a time. This means if one part of the code takes a long time to finish, the rest has to wait.

But what if we want to do multiple things at once—like downloading a file while showing a loading animation?

This is where multithreading comes in.

What is a Thread?

A thread is like a mini-program that runs inside our Ruby program. With multithreading, we can run multiple threads at the same time.

In Ruby, we can create threads using the Thread class.


Creating Threads in Ruby

We use the Thread.new method to create a new thread. Here's its syntax:

Thread.new do
  # Code to run in a new thread
end

This syntax creates a child thread inside our main program.

But before we use this syntax to create an actual thread, let's first understand the difference between a main thread and child threads.

Main Thread vs Child Threads

When we run a Ruby program, it always starts with a main thread. Any threads we create using Thread.new are child threads. For example,

# Create a new thread (child thread)
new_thread = Thread.new do
  puts "Welcome to a new thread!"
  puts "Exiting the thread..."
end

# Main thread starts here

# Wait for the child thread to finish
new_thread.join

# Print a message
puts "Welcome to the main program."

Output

Welcome to a new thread!
Exiting the thread...
Welcome to the main program.

Here, we created a child thread named new_thread that prints a set of messages.

The join method tells Ruby to wait for the thread to finish before moving on to the main thread.

Tip: Try removing the join method from this program. You'll see that the execution moves on to the main program right away.


Concurrent Threads with Sleep

We can use the sleep method to pause our thread for a specified amount of time. Let's use it to make the main thread and the child thread execute concurrently:

# Child thread
thread = Thread.new do
    
  # Loop code 3 times
  3.times do
    puts "Child thread working..."
        
    # Pause the thread for 1 second
    sleep(1)
  end
end


# Main thread

# Loop code 3 times
3.times do
  puts "Main thread working..."
        
  # Pause the thread for 1 second
  sleep(1)
end

# Wait for the child thread to end
thread.join

Sample Output

Main thread working...
Child thread working...
Main thread working...
Child thread working...
Main thread working...
Child thread working...

Here, the main and child threads each run a times loop three times. In each iteration of the loops, the threads pause for one second.

1. Role of the 'join' Method

Notice that we have used the join method only at the end of the program. So, the main thread starts its own loop without waiting for the child thread to finish.

2. Role of the 'sleep' Method

Thanks to the sleep method in the loop of the main thread, the child thread will be executed in between the pauses.

Similarly, the sleep method in the child thread ensures that the main thread gets executed in between the pauses.

3. Concurrent Threads

As a result, the main and child threads execute concurrently (interleaved). This means the outputs from the main and child threads appear mixed (not all from one, then the other).

Notes:

  • Ruby does not guarantee exact interleaving or timing. Thread scheduling depends on the Ruby implementation and operating system.
  • In standard Ruby (MRI), a Global Interpreter Lock (GIL) prevents true parallel execution of threads for CPU-bound tasks. However, threads are still useful for I/O-bound tasks, i.e., tasks concerning input/output.
  • You can remove thread.join at the end of this program and it'll still work. However, there'll be no guarantee that the main thread will wait for the child thread to execute completely. Thus, it's safer to use join.

Example 1: Basic Ruby Multithreading

thread = Thread.new do
  puts "Thread starting..."
  sleep(2)
  puts "Thread done!"
end

puts "Waiting for thread..."

thread.join

puts "All done!"

Output

Waiting for thread...
Thread starting...
Thread done!
All done!

Here's how this program works:

  1. The main thread first executes the puts "Waiting for thread...".
  2. It then encounters thread.join. As a result, it waits for the child thread (stored in the thread variable) to complete.
  3. In the child thread, puts "Thread starting..." is executed first.
  4. The child thread then pauses for two seconds due to sleep(2).
  5. After the pause, puts "Thread done!" is executed.
  6. Because the child thread has completed execution, the program goes back to the main thread.
  7. Finally, the main thread executes puts "All done!"

Example 2: Running Multiple Threads

thread1 = Thread.new do
  puts "Thread 1 starting"
  sleep(1)
  puts "Thread 1 finished"
end

thread2 = Thread.new do
  puts "Thread 2 starting"
  sleep(1)
  puts "Thread 2 finished"
end

thread3 = Thread.new do
  puts "Thread 3 starting"
  sleep(1)
  puts "Thread 3 finished"
end

# Wait for all threads to finish
thread1.join
thread2.join
thread3.join

puts "\nAll threads finished!"

Sample Output

Thread 1 starting
Thread 2 starting
Thread 3 starting
Thread 1 finished
Thread 2 finished
Thread 3 finished

All threads finished!

Here, we created three threads (thread1, thread2, and thread3) and waited for them all to complete.

Notice that we created the threads separately, resulting in a long chain of codes:

thread1 = Thread.new do
  # Code for thread1
end

thread2 = Thread.new do
  # Code for thread2
end

thread3 = Thread.new do
  # Code for thread3
end

We can rectify this issue by creating the threads inside a loop. Let's see how in the next example.


Example 3: Create Multiple Threads Using Loop and Array

Let's write a cleaner version of the previous example by creating the threads inside a loop and adding them to an array:

# Create an array to store multiple threads
threads = []

# Run a loop 3 times to create 3 separate threads
3.times do |i|
    
  # Create a new thread and
  # add it to the threads array
  threads << Thread.new do
    puts "Thread #{i + 1} starting"
    sleep(1)
    puts "Thread #{i + 1} finished"
  end
end

# Iterate through the array
# Wait for each thread to execute
threads.each(&:join)

puts "\nAll threads finished!"

Sample Output

Thread 1 starting
Thread 2 starting
Thread 3 starting
Thread 2 finished
Thread 1 finished
Thread 3 finished

All threads finished!

1. Creating the Threads with Loop

First, we ran a times loop three times to create three threads. In each iteration of the loop, a new thread is created and appended to the threads array.

3.times do |i|
  threads << Thread.new do
    # Thread code
  end
end

2. The Main Thread

In the main thread, we use an each loop to iterate through the threads array.

In each iteration of the loop, we use join with the respective thread to ensure that the thread is executed completely:

threads.each(&:join)

The above code is a shorthand for the following:

threads.each { |thread| thread.join }

You can replace the each loop shorthand in the example with the traditional each loop code above.

Note: The output of this program may vary with each execution.


Shared Data and Race Conditions

All threads share the same memory. So, if multiple threads try to change the same variable at the same time, unexpected things can happen. For example,

# Create a counter variable
counter = 0

# Loop 3 times to create 3 threads
# And append them to the threads array
threads = 3.times.map do
  Thread.new do
    10.times do
      # Increment counter
      counter += 1
    end
  end
end

# Wait for the threads to execute
threads.each(&:join)

# Print the value of counter
puts "Counter: #{counter}"

Expected Output

Counter: 30

Possible Output 1

Counter: 26

Possible Output 2

Counter: 29

Here, we created the counter variable in the main thread and initialized it to 0. Thus, this variable will be shared by all child threads.

1. Creating the Threads

Then, we created three child threads using the following code:

threads = 3.times.map do
  Thread.new do
    # Code
  end
end

We use map to create an array of threads by collecting the result of each Thread.new call.

Each thread runs its own times loop with 10 iterations. In each iteration of the loops, the value of the counter variable is increased by 1.

Thread.new do
  10.times do
    # Increment counter
    counter += 1
  end
end

2. Ideal Scenario

Ideally, each thread executes without interference from the others, updating the counter variable in the following way:

Thread Loop Operation counter (Final Value)
1 Increment the value of counter 10 times. 10
2 Increment the value of counter 10 times. 20
3 Increment the value of counter 10 times. 30

3. Practical Scenario

In reality, the multiple threads might try to simultaneously update the counter variable.

For instance, if the first thread updates the value of counter to 6, it's possible that another thread also updates its value to 6 at the same time.

So, instead of the first thread updating counter to 6 and then the other thread updating it to 7, the update made by the first thread gets overwritten.

In this scenario, counter will be updated to 6 and the final output will be less than 30 as a result.

Note: The scenario above is known as a race condition, when multiple threads attempt to read, modify, and write a shared resource without coordination.

In Ruby, we can prevent race conditions using a Mutex.


Using Mutex to Prevent Race Conditions

To prevent race conditions, we use a Mutex (mutual exclusion), which locks a section of code so only one thread can run it at a time.

You can think of a Mutex like a lock on a bathroom door. Only one person (thread) can go inside at a time.

In general, we use Mutex to control access to shared resources when multiple threads are running concurrently.

Syntax

mutex_object = Mutex.new

# Other code

mutex_object.synchronize do
  # Code that accesses shared resources
end

Example 4: Ruby Mutex

Now, let's use a Mutex to prevent race conditions from occurring in our previous program:

counter = 0

# Create a Mutex object
mutex = Mutex.new

threads = 3.times.map do
  Thread.new do
    10.times do
            
      # Update counter inside the Mutex block
      mutex.synchronize do
        counter += 1
      end
            
    end
  end
end

threads.each(&:join)
puts "Counter: #{counter}"

Output

Counter: 30

Here, we created a Mutex object named mutex. We then used this object to lock the thread when it's accessing the counter variable (since it's a shared resource).

mutex.synchronize do
  counter += 1
end

So, when a thread reaches this part of the code, it tries to lock on to the block of code inside mutex.synchronize do:

  • If mutex is already locked by another thread, the current thread waits until it is unlocked.
  • Once mutex is acquired (locked), the thread executes the block and increments counter by 1.
  • After the block finishes, mutex is automatically unlocked, allowing other waiting threads to acquire it.

Since no thread is interfering with the execution of another, we always get the following output:

Counter: 30

Handling Errors in Threads

If an error happens inside a thread, it won't crash the main program but the thread will stop. For example,

# A thread that raises an error
Thread.new do
  raise "Something went wrong"
end

# Pause main thread for 1 second
# So that the child thread can execute
sleep(1)

puts "Main program continues"

Output

#<Thread:0x00007c501ef63bd8 /tmp/ZXZLrHrmDX/main.rb:1 run> terminated with exception (report_on_exception is true):
/tmp/ZXZLrHrmDX/main.rb:2:in 'block in <main>': Something went wrong (RuntimeError)
Main program continues

Here, we created a thread that manually raises an error with the raise keyword. This stops the execution of the thread.

You can handle such exceptions using begin...rescue:

Thread.new do
    
  # Put code that may cause error inside 'begin'
  begin
    raise "Error in thread"
    
  # Rescue the error using object 'e'
  rescue => e
    puts "Caught error: #{e.message}"

  end
end

sleep(1)

puts "Main program continues"

Output

ERROR!
Caught error: Error in thread
Main program continues

Here, we've kept the code that might cause an error inside the begin statement. If an error occurs, the rescue block handles the exception by printing an error message.

This way, even if a thread causes an error, its execution won't be stopped abruptly.

To learn more, visit Ruby Exception Handling.


More on Ruby Multithreading

Why to use threads, and when to avoid them?

1. When to Use Threads

Use threads when you want to:

  • Perform multiple tasks at once.
  • Avoid blocking the main program (e.g., during long-running tasks).
  • Improve performance in I/O-heavy programs.

Note: Ruby uses a Global Interpreter Lock (GIL), which limits how much true parallelism is possible for CPU-bound tasks. But threads still help with tasks that wait on input/output (like file operations, network requests, or database access).

2. When Not to Use Threads

  • For heavy CPU-bound tasks in Ruby (GIL limits performance).
  • If you need true parallelism. In such cases, consider using processes or external libraries like parallel or concurrent-ruby.
Best practices for threads.
  • Always use join to wait for threads to complete.
  • Avoid shared variables unless using thread-safe methods.
  • Use Mutex for safe access to shared data.
  • Handle exceptions inside threads.
  • Keep thread logic simple and focused.
What is the difference between a thread and a process?

A process has its own memory, while threads share memory within the same process.

And while threads are lighter and faster to create, they don't offer true parallelism (unlike processes).

Variables declared inside a thread are local to it.

While threads share variables and resources that are declared in the main thread, they do not share variables that are declared inside them.

In other words, variables declared inside a thread are local to it. For example,

thread1 = Thread.new do
  # Create a local variable 'num'
  num = 5
  puts num
end

# Invalid: Attempt to access 'num' in another thread
thread2 = Thread.new do
  puts num
end

thread1.join
thread2.join

# Invalid: Attempt to access 'num' in main thread
puts num

Output

5
#<Thread:0x00007ddbefa53978 /tmp/XjZ6AVlftj/main.rb:8 run> terminated with exception (report_on_exception is true):
/tmp/XjZ6AVlftj/main.rb:9:in 'block in <main>': undefined local variable or method 'num' for main (NameError)

Here, num is a variable that's local to the scope of thread1. As a result, we cannot access it from outside that thread.

Using the join method at the end of the thread.

You can attach the join method to the end statement of a thread. For example,

thread1 = Thread.new do
  puts "First thread"
end.join

thread2 = Thread.new do
  puts "Second thread"
end.join

puts "Main thread"

Output

First thread
Second thread
Main thread

As you can see, we've used end.join to make the program wait for the threads to execute. This syntax is common in short thread blocks and helps reduce clutter.

The above program is equivalent to:

thread1 = Thread.new do
  puts "First thread"
end

thread2 = Thread.new do
  puts "Second thread"
end

thread1.join
thread2.join

puts "Main thread"
Check thread status using the alive? method.

We can check the status of a thread using the alive? method. For example,

thread = Thread.new do
  sleep(2)
end

puts "Thread alive? #{thread.alive?}"  # Output: true
sleep(3)
puts "Thread alive? #{thread.alive?}"  # Output: false

Here, thread pauses for two seconds before ending. So, it will be "alive" (running or sleeping) for two seconds.

As the child thread is sleeping, we check if it's alive in the main thread. Here's how the checking works:

1. The First Check

When we check the thread for the first time, it's still running since two seconds haven't elapsed. Therefore, thread.alive? returns true.

2. The Second Check

After the first check, the main thread sleeps for three seconds. During this time, the child thread has already finished its execution.

Thus, the second thread.alive? returns false.

Get the return value of the thread using the value method.

We can use the value method to get the last evaluated expression of a thread. For example,

thread = Thread.new do
  5 * 2
end

puts thread.value

# Output: 10

Here, thread.value gives us the result of the thread operation 5 * 2, which is 10.

Note: With thread.value, the thread blocks until the result is available. This is similar to calling join and then retrieving the result.

Did you find this article helpful?

Our premium learning platform, created with over a decade of experience and thousands of feedbacks.

Learn and improve your coding skills like never before.

Try Programiz PRO
  • Interactive Courses
  • Certificates
  • AI Help
  • 2000+ Challenges