Fibers & Lazy Evaluation
Reviewed & published by Brayan K
A Fiber is a block of code you can pause with Fiber.yield and resume with resume, cooperatively passing control and values back and forth between the fiber and its caller.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll use fibers to build lazy, on-demand sequences and see how Ruby's Enumerator is powered by them.
What You'll Learn in This Lesson
- Creating fibers with Fiber.new
- Pausing with Fiber.yield, continuing with resume
- Cooperative pausing (no preemption)
- Passing values into and out of a fiber
- Building lazy, on-demand sequences
- How Enumerator uses fibers, and fibers vs threads
1️⃣ Pause and Resume with Fibers
Create a fiber with Fiber.new { ... }. Calling resume runs it until it hits Fiber.yield, which pauses it and returns a value to the caller. The next resume picks up right after that yield — local variables and all.
# A Fiber is a block of code you can PAUSE and RESUME at will.
# Fiber.yield pauses; Fiber#resume continues from where it left off.
fib = Fiber.new do
puts "start"
Fiber.yield "first pause" # hands control back to the caller
puts "resumed"
Fiber.yield "second pause"
puts "finishing"
"done" # the final value of resume
end
puts fib.resume # runs to the first Fiber.yield
puts fib.resume # continues to the second Fiber.yield
puts fib.resume # runs to the end2️⃣ Two-Way Communication
Fibers are coroutines: Fiber.yield x sends x out to the caller, and resume(y) sends y back in, becoming the return value of that yield. The first resume usually just primes the fiber up to its first pause.
# resume can pass a value INTO the fiber; Fiber.yield returns it.
# This makes a fiber a two-way, cooperative coroutine.
adder = Fiber.new do
total = 0
loop do
n = Fiber.yield total # pause, returning the running total;
total += n # when resumed, n is the value passed in
end
end
adder.resume # prime it: run up to the first yield (total = 0)
puts adder.resume(10) # add 10 -> 10
puts adder.resume(5) # add 5 -> 15
puts adder.resume(100) # add 100 -> 1153️⃣ Lazy Sequences & the Enumerator Connection
Because a fiber produces one value per resume, it's a natural generator: you can model an infinite stream and only compute as far as you pull. Ruby's Enumerator is built on this exact mechanism — enum.next resumes an internal fiber to produce the next item.
# Building a lazy sequence with a Fiber: produce values only on demand.
def naturals_fiber
Fiber.new do
n = 1
loop do
Fiber.yield n # produce the next number, then pause
n += 1
end
end
end
gen = naturals_fiber
first_five = 5.times.map { gen.resume }
p first_five
# Enumerator is built ON TOP of fibers — .next uses one under the hood.
enum = Enumerator.new do |y|
y << "a"
y << "b"
y << "c"
end
puts enum.next # internally resumes a fiber
puts enum.next
puts enum.peek # look without advancing
puts enum.next🎯 Your Turn
The fiber should pause after each color. Replace each ___ with the call that pauses the fiber and hands a value to the caller.
# 🎯 YOUR TURN — fill in the ___ so the fiber pauses on each color.
colors = Fiber.new do
___ "red" # 👉 which call pauses the fiber and returns a value?
___ "green"
___ "blue"
end
puts colors.resume # should print red
puts colors.resume # should print green
puts colors.resume # should print blue
# ✅ Expected output:
# red
# green
# blueCommon Errors (and the fix)
- "FiberError: dead fiber called" — ❌ You resumed a fiber that already finished. ✅ Stop resuming after its last value, or use an Enumerator.
- "can't yield from root fiber" — ❌ Fiber.yield called outside any fiber. ✅ Only yield inside a Fiber.new block.
- Fiber never pauses — ❌ You used yield (the block keyword) instead of Fiber.yield. ✅ Use Fiber.yield.
- First value is nil — ❌ You forgot to prime a two-way fiber. ✅ Call resume once before sending values in.
- Expecting parallelism — ❌ Fibers don't run concurrently. ✅ Reach for threads (next lesson) when you need overlap.
Pro Tips
- 💡 Prefer Enumerator: for most generators, Enumerator.new wraps a fiber and adds peek, lazy, and graceful exhaustion.
- 💡 State lives on: local variables inside a fiber persist across resume calls — perfect for counters and state machines.
- 💡 Fiber schedulers: modern Ruby uses fibers under the hood for non-blocking I/O via the Fiber scheduler interface.
Mini-Challenge: A Lazy ID Generator
Build a fiber-based ID generator that yields zero-padded IDs on demand, and run two independent generators side by side. Run with ruby ids.rb.
# 🎯 MINI-CHALLENGE: a lazy ID generator using a Fiber.
# Save as ids.rb and run with: ruby ids.rb
#
# 1. Write id_generator(prefix) returning a Fiber.
# 2. Each resume yields "PREFIX-001", "PREFIX-002", ... (zero-padded to 3).
# 3. Create TWO independent generators and interleave their resumes.
#
# ✅ Expected output:
# USER-001
# USER-002
# ORD-001
# USER-003
# ORD-002
# your code here📋 Quick Reference — Fibers
| Goal | Ruby |
|---|---|
| Create a fiber | Fiber.new { ... } |
| Pause & send out | Fiber.yield value |
| Resume & send in | fiber.resume(arg) |
| Generator API | Enumerator.new { |y| ... } |
| Pull next value | enum.next / enum.peek |
| vs threads | cooperative, no parallelism |
🎉 Lesson Complete!
- ✅ A fiber pauses with Fiber.yield and continues with resume
- ✅ Fibers are cooperative — no preemption, no race conditions
- ✅ resume(x) and Fiber.yield y pass values both ways
- ✅ Fibers make natural lazy, on-demand generators
- ✅ Enumerator is built on fibers; threads give parallelism, fibers don't
Practice quiz
How do you create a Fiber?
- Fiber.start { ... }
- Fiber.run { ... }
- Fiber.new { ... }
- fiber { ... }
Answer: Fiber.new { ... }. You create a fiber with Fiber.new { ... } and run it with resume.
What pauses a fiber from inside its block?
- Fiber.yield
- yield
- Fiber.pause
- return
Answer: Fiber.yield. Fiber.yield pauses the fiber and hands control (and a value) back to the caller.
What method continues a paused fiber?
- fiber.next
- fiber.call
- fiber.yield
- fiber.resume
Answer: fiber.resume. fiber.resume continues from where the fiber last paused.
How is a fiber different from a thread?
- Fibers run in parallel
- Fibers are cooperative — they only advance when resumed, with no preemption
- Threads can't share data
- Fibers are preemptive
Answer: Fibers are cooperative — they only advance when resumed, with no preemption. A fiber only runs between resume and the next yield; it is cooperative, so there is no preemption or race conditions.
What happens if you resume a fiber that has already finished?
- It raises FiberError (dead/terminated fiber)
- It returns nil
- It restarts from the top
- It blocks forever
Answer: It raises FiberError (dead/terminated fiber). Resuming a finished fiber raises FiberError about a dead/terminated fiber.
How does a value get INTO a fiber?
- Through Fiber.yield's argument
- Via a global variable
- As the return value of Fiber.yield, via resume(arg)
- You can't pass values in
Answer: As the return value of Fiber.yield, via resume(arg). resume(arg) sends arg in; it becomes the return value of the Fiber.yield that paused the fiber.
Which Ruby class is built on top of fibers internally?
- Array
- Enumerator
- Hash
- Proc
Answer: Enumerator. Enumerator uses a fiber internally; enum.next resumes it to produce the next value.
Why does the first resume of a two-way fiber often just 'prime' it?
- To allocate memory
- To check for errors
- To start a thread
- To run it up to its first yield before you feed values in
Answer: To run it up to its first yield before you feed values in. The first resume runs the fiber to its first pause; later resumes can then send values in.
Can fibers achieve true parallelism on their own?
- Yes, always
- No — only one fiber runs at a time
- Only on multiple cores
- Only with a mutex
Answer: No — only one fiber runs at a time. Fibers are cooperative and run one at a time; for parallelism you reach for threads.
Inside a Fiber block, using the plain `yield` keyword instead of `Fiber.yield` will...
- Pause the fiber correctly
- Raise immediately
- Fail to pause the fiber (it's the block keyword, not the fiber pause)
- Resume the caller
Answer: Fail to pause the fiber (it's the block keyword, not the fiber pause). Plain yield calls the block; you must use Fiber.yield to actually pause the fiber.
Continue this course
- Previous: Refinements
- Next: Threads, Mutex & Ractor — Concurrency with threads, safe state via Mutex, and a look at Ractor
- Quick reference: Ruby cheat sheet
Frequently asked questions
What is a Fiber in Ruby?
A Fiber is a lightweight unit of code that you can pause and resume cooperatively. You create one with Fiber.new { ... }, run it with fiber.resume, and pause it from the inside with Fiber.yield. Unlike a thread, a fiber never runs on its own — it only advances when something explicitly resumes it, so there is no preemption and no race conditions. This makes fibers ideal for building generators, lazy sequences, and state machines where you want to suspend computation midway and pick it up later.
How is a fiber different from a thread?
A thread is scheduled by the operating system and can be interrupted at any moment, so two threads sharing data can collide and you need locks. A fiber is scheduled by your own code: it runs only between a resume and the next Fiber.yield, and control passes back and forth deterministically. Fibers are therefore cheaper and simpler than threads but cannot achieve parallelism on their own — only one fiber runs at a time. Use threads for overlapping I/O or true concurrency, and fibers for cooperative, predictable suspension.
How do fibers relate to Enumerator and lazy evaluation?
Ruby's Enumerator uses a fiber internally to implement external iteration. When you call enum.next, Ruby resumes a fiber that runs the enumerator's block until the next yielded value, then pauses it again — which is exactly how a generator produces one item at a time without computing the whole sequence. That same pause/resume mechanism is what lets lazy enumerators and fiber-based generators model infinite streams: values are produced on demand, only as far as the consumer pulls them.
Can I pass values into and out of a fiber?
Yes — fibers are two-way. Inside the fiber, Fiber.yield value sends value out as the return of the current resume call. Outside, fiber.resume(arg) sends arg in, and it becomes the return value of the Fiber.yield that paused the fiber. This bidirectional flow turns a fiber into a coroutine: the caller and the fiber take turns, each handing data to the other. The first resume usually 'primes' the fiber by running it up to its first yield before you start feeding values in.