Overview
Ruby provides four essential keywords for controlling program flow within loops, iterators, and exception handling: break
, next
, redo
, and retry
. These keywords allow developers to alter the normal execution sequence of loops and blocks, providing fine-grained control over iteration behavior.
The break
keyword terminates the innermost enclosing loop or iterator, returning control to the statement following the loop. The next
keyword skips the remaining statements in the current iteration and moves to the next iteration. The redo
keyword restarts the current iteration from the beginning without evaluating the loop condition again. The retry
keyword restarts a block or method from the beginning and is primarily used in exception handling contexts.
# Basic break usage
[1, 2, 3, 4, 5].each do |n|
break if n > 3
puts n
end
# Output: 1, 2, 3
# Basic next usage
[1, 2, 3, 4, 5].each do |n|
next if n.even?
puts n
end
# Output: 1, 3, 5
# Basic redo usage with condition
i = 0
[1, 2, 3].each do |n|
i += 1
redo if i < 2 && n == 1
puts "#{n}: #{i}"
end
These keywords work within blocks passed to iterators like each
, map
, select
, and traditional loop constructs like while
, for
, and loop
. Understanding their behavior is crucial for writing effective Ruby code that handles complex iteration scenarios.
Basic Usage
Break Statement
The break
keyword immediately exits the current loop or iterator, optionally returning a value. When used in a loop, execution continues with the first statement after the loop. In iterator blocks, break
causes the iterator method to return the specified value or nil
.
# Breaking from a while loop
count = 0
while true
count += 1
break if count >= 5
puts count
end
puts "Loop ended at #{count}"
# Breaking from an iterator with return value
result = [1, 2, 3, 4, 5].each do |n|
break "found it!" if n == 3
puts n
end
puts result # "found it!"
# Breaking from nested loops - only breaks innermost
(1..3).each do |i|
(1..3).each do |j|
break if j == 2
puts "#{i}, #{j}"
end
puts "Outer loop #{i} continues"
end
When break
appears in a block without an explicit value, the iterator method returns nil
. The break value becomes the return value of the entire iterator expression.
Next Statement
The next
keyword skips the remaining code in the current iteration and proceeds to the next iteration of the loop. Unlike break
, which exits the entire loop, next
only affects the current iteration.
# Skipping even numbers
(1..10).each do |n|
next if n.even?
puts "Processing #{n}"
end
# Next with conditional logic
users = ['alice', '', 'bob', nil, 'charlie']
users.each do |user|
next if user.nil? || user.empty?
puts "Hello, #{user.capitalize}"
end
# Next in map operations
numbers = [1, 2, 3, 4, 5]
doubled = numbers.map do |n|
next if n.even? # returns nil for even numbers
n * 2
end
# => [2, nil, 6, nil, 10]
When next
appears in an iterator block, it causes the current iteration to end immediately and return nil
to the collection being built (in methods like map
).
Redo Statement
The redo
keyword restarts the current iteration from the beginning of the block or loop body. The loop condition is not re-evaluated, and loop variables maintain their current values. This creates the potential for infinite loops if not used carefully.
# Redo with user input validation
attempts = 0
loop do
attempts += 1
puts "Attempt #{attempts}: Enter a number (1-10):"
input = gets.chomp.to_i
if input < 1 || input > 10
puts "Invalid input!"
redo if attempts < 3
puts "Too many attempts"
break
end
puts "You entered: #{input}"
break
end
# Redo in iterator with condition
values = [1, 'invalid', 3, 4]
results = []
values.each_with_index do |val, idx|
if val.is_a?(String)
puts "Converting string at index #{idx}"
val = idx + 1 # Convert to number
redo # Process the corrected value
end
results << val * 2
end
The redo
keyword is particularly useful for retry logic within iterations, error recovery, and data validation scenarios where the current iteration needs to be repeated with modified conditions.
Retry Statement
The retry
keyword restarts the execution of a block or method from the very beginning. It's most commonly used in exception handling to retry operations after handling recoverable errors.
# Basic retry in exception handling
attempts = 0
begin
attempts += 1
puts "Attempt #{attempts}"
# Simulate unreliable operation
raise "Network error" if rand < 0.7
puts "Operation succeeded!"
rescue => e
puts "Error: #{e.message}"
retry if attempts < 3
puts "Max attempts reached"
end
# Retry with exponential backoff
def fetch_data(url)
attempts = 0
begin
attempts += 1
# Simulate network request
raise "Connection timeout" if rand < 0.5
"Data from #{url}"
rescue => e
if attempts <= 3
sleep_time = 2 ** attempts
puts "Retry in #{sleep_time} seconds..."
sleep(sleep_time)
retry
else
raise "Failed after #{attempts} attempts: #{e.message}"
end
end
end
Advanced Usage
Breaking with Values in Complex Scenarios
Ruby allows break
to return complex data structures and supports sophisticated flow control patterns in nested iterators and blocks.
# Breaking with complex return values
result = (1..100).each_with_object({}) do |num, hash|
if num > 50
break { status: :incomplete, processed: hash.keys.length, reason: "limit exceeded" }
end
next if num % 7 != 0 # Skip non-multiples of 7
hash[num] = num ** 2
end
# Multi-level break simulation using exceptions
class BreakAll < StandardError; end
begin
(1..5).each do |i|
(1..5).each do |j|
(1..5).each do |k|
puts "#{i}, #{j}, #{k}"
raise BreakAll if i * j * k > 20
end
end
end
rescue BreakAll
puts "Broke out of all loops"
end
# Using break in method definitions with blocks
def process_until_condition(items, &block)
items.each_with_index do |item, index|
result = block.call(item, index)
break result if result.is_a?(Hash) && result[:stop]
end
end
data = ['a', 'b', 'c', 'd', 'e']
final_result = process_until_condition(data) do |item, idx|
puts "Processing #{item} at index #{idx}"
if item == 'c'
{ stop: true, message: "Found target", index: idx }
else
item.upcase
end
end
Advanced Redo Patterns
Complex redo scenarios often involve state management and condition tracking to prevent infinite loops while enabling sophisticated retry logic.
# Redo with state tracking for data processing
def process_with_correction(data)
results = []
corrections = {}
data.each_with_index do |item, index|
correction_count = corrections[index] ||= 0
case item
when String
if item.empty? && correction_count == 0
corrections[index] += 1
item = "default_#{index}"
redo
elsif item =~ /\d+/ && correction_count == 1
corrections[index] += 1
item = item.gsub(/\d+/, '')
redo
end
when Numeric
if item < 0 && correction_count == 0
corrections[index] += 1
item = item.abs
redo
end
end
results << { original_index: index, value: item, corrections: correction_count }
end
results
end
# Redo with dynamic condition evaluation
class DataProcessor
def initialize(rules)
@rules = rules
end
def process(items)
items.map.with_index do |item, idx|
retry_count = 0
begin
@rules.each do |rule|
if rule[:condition].call(item)
item = rule[:transform].call(item)
retry_count += 1
redo if retry_count < rule[:max_retries]
end
end
item
end
end
end
end
rules = [
{
condition: ->(x) { x.is_a?(String) && x.length < 3 },
transform: ->(x) { x + "_extended" },
max_retries: 2
}
]
processor = DataProcessor.new(rules)
Sophisticated Retry Mechanisms
Advanced retry patterns incorporate circuit breaker patterns, jitter, and sophisticated error classification to handle complex failure scenarios.
# Circuit breaker with retry
class CircuitBreaker
def initialize(failure_threshold: 5, reset_timeout: 60)
@failure_threshold = failure_threshold
@reset_timeout = reset_timeout
@failure_count = 0
@last_failure_time = nil
@state = :closed # :closed, :open, :half_open
end
def call(&block)
if @state == :open && Time.now - @last_failure_time < @reset_timeout
raise "Circuit breaker is OPEN"
end
@state = :half_open if @state == :open
attempts = 0
begin
attempts += 1
result = block.call
reset_circuit
result
rescue => e
@failure_count += 1
@last_failure_time = Time.now
if @failure_count >= @failure_threshold
@state = :open
raise "Circuit breaker opened after #{@failure_count} failures"
end
if attempts <= 3
sleep_with_jitter(attempts)
retry
else
raise e
end
end
end
private
def reset_circuit
@failure_count = 0
@state = :closed
@last_failure_time = nil
end
def sleep_with_jitter(attempt)
base_delay = 2 ** attempt
jitter = rand(0.5..1.5)
sleep(base_delay * jitter)
end
end
# Usage with complex retry logic
breaker = CircuitBreaker.new(failure_threshold: 3, reset_timeout: 30)
def unreliable_api_call
raise "API Error" if rand < 0.6
"Success!"
end
result = breaker.call { unreliable_api_call }
Error Handling & Debugging
Common Break and Next Pitfalls
Understanding the scope and behavior of break
and next
in different contexts prevents common programming errors and unexpected behavior.
# Pitfall: Break in nested blocks
def problematic_break
[1, 2, 3].each do |outer|
[4, 5, 6].each do |inner|
# This break only exits the inner each, not the method
break if inner == 5
puts "#{outer}, #{inner}"
end
puts "Outer continues: #{outer}"
end
end
# Solution: Use exceptions for multi-level breaks
class MultiBreak < StandardError; end
def correct_multi_break
begin
[1, 2, 3].each do |outer|
[4, 5, 6].each do |inner|
raise MultiBreak if inner == 5
puts "#{outer}, #{inner}"
end
puts "Outer continues: #{outer}"
end
rescue MultiBreak
puts "Multi-level break executed"
end
end
# Debugging break behavior in different contexts
def debug_break_behavior
puts "=== In method with block ==="
result = [1, 2, 3, 4, 5].map do |n|
puts "Processing #{n}"
if n == 3
break "early exit" # Causes map to return "early exit"
end
n * 2
end
puts "Result: #{result.inspect}"
puts "\n=== In proc vs lambda ==="
my_proc = Proc.new do |n|
break "proc break" if n == 3 # This would cause LocalJumpError
n * 2
end
my_lambda = lambda do |n|
return "lambda return" if n == 3 # This works fine
n * 2
end
# Safe usage patterns
begin
result = [1, 2, 3, 4].map(&my_proc)
rescue LocalJumpError => e
puts "LocalJumpError: #{e.message}"
end
end
Redo and Retry Error Patterns
Improper use of redo
and retry
can lead to infinite loops and stack overflow errors. Implementing proper safeguards is essential.
# Dangerous redo pattern (infinite loop)
def dangerous_redo
counter = 0
[1, 2, 3].each do |n|
counter += 1
puts "Processing #{n}, attempt #{counter}"
# BUG: This could loop forever
if n == 2 && counter < 100 # Weak condition
redo
end
end
end
# Safe redo pattern with proper guards
def safe_redo_with_guards
[1, 2, 3].each_with_index do |n, idx|
attempt_count = 0
max_attempts = 3
begin
attempt_count += 1
puts "Processing #{n} (attempt #{attempt_count})"
# Simulate processing that might need redo
if n == 2 && attempt_count == 1
raise "Simulated error"
end
puts "Successfully processed #{n}"
rescue => e
if attempt_count < max_attempts
puts "Error: #{e.message}, retrying..."
redo
else
puts "Max attempts reached for #{n}, skipping"
next
end
end
end
end
# Retry with proper error classification
def classified_retry_pattern
retryable_errors = [Errno::ECONNREFUSED, Timeout::Error, Net::ReadTimeout]
permanent_errors = [ArgumentError, NoMethodError]
attempts = 0
max_attempts = 5
begin
attempts += 1
puts "Attempt #{attempts}"
# Simulate various error types
error_type = [:retryable, :permanent, :success].sample
case error_type
when :retryable
raise Errno::ECONNREFUSED, "Connection refused"
when :permanent
raise ArgumentError, "Invalid argument"
when :success
return "Operation completed successfully"
end
rescue *retryable_errors => e
if attempts < max_attempts
backoff = [2 ** attempts, 30].min # Cap at 30 seconds
puts "Retryable error: #{e.message}. Retrying in #{backoff}s..."
sleep(backoff)
retry
else
raise "Max retry attempts (#{max_attempts}) exceeded: #{e.message}"
end
rescue *permanent_errors => e
raise "Permanent error, not retrying: #{e.message}"
rescue => e
puts "Unexpected error type: #{e.class} - #{e.message}"
raise
end
end
Performance & Memory
Performance Implications of Flow Control
The choice of flow control keywords can significantly impact performance, especially in tight loops and high-volume data processing scenarios.
require 'benchmark'
# Performance comparison: break vs conditions
def benchmark_flow_control
large_array = (1..1_000_000).to_a
Benchmark.bm(15) do |x|
x.report("with break:") do
large_array.each do |n|
break if n > 500_000
# Minimal processing
n * 2
end
end
x.report("with condition:") do
large_array.each do |n|
next if n > 500_000
# Minimal processing
n * 2
end
end
x.report("select + break:") do
large_array.select do |n|
break if n > 500_000
n.even?
end
end
x.report("take_while:") do
large_array.take_while { |n| n <= 500_000 }.select(&:even?)
end
end
end
# Memory usage patterns with different approaches
def memory_usage_comparison
# Memory-efficient early termination
def find_first_match_break(data)
data.each do |item|
if complex_condition?(item)
return process_item(item)
end
end
nil
end
# Memory-inefficient - processes all items
def find_first_match_filter(data)
data.select { |item| complex_condition?(item) }
.first
&.then { |item| process_item(item) }
end
# Memory-efficient with lazy evaluation
def find_first_match_lazy(data)
data.lazy
.select { |item| complex_condition?(item) }
.first
&.then { |item| process_item(item) }
end
private
def complex_condition?(item)
item.to_s.length > 5 && item.to_s.include?('special')
end
def process_item(item)
{ processed: item, timestamp: Time.now }
end
end
# Optimizing retry patterns for performance
class PerformantRetryManager
def initialize(base_delay: 0.1, max_delay: 10, multiplier: 2)
@base_delay = base_delay
@max_delay = max_delay
@multiplier = multiplier
end
def with_retry(max_attempts: 3, &block)
attempts = 0
start_time = Time.now
begin
attempts += 1
result = block.call
# Log performance metrics
if attempts > 1
total_time = Time.now - start_time
puts "Succeeded after #{attempts} attempts in #{total_time.round(3)}s"
end
result
rescue => e
if attempts < max_attempts
delay = calculate_delay(attempts)
sleep(delay)
retry
else
total_time = Time.now - start_time
raise "Operation failed after #{attempts} attempts in #{total_time.round(3)}s: #{e.message}"
end
end
end
private
def calculate_delay(attempt)
delay = @base_delay * (@multiplier ** (attempt - 1))
[delay, @max_delay].min
end
end
Memory Leak Prevention
Improper use of flow control in long-running processes can lead to memory leaks, especially when combined with closures and object retention.
# Memory leak example with redo
class LeakyProcessor
def initialize
@cache = {}
end
def process_with_leak(items)
items.each_with_index do |item, idx|
attempt = 0
begin
attempt += 1
# BUG: Cache grows indefinitely on redo
cache_key = "#{idx}_#{attempt}_#{Time.now.to_f}"
@cache[cache_key] = expensive_computation(item)
# Simulate failure requiring redo
if item.to_s.include?('retry') && attempt < 5
redo
end
yield @cache[cache_key]
ensure
# BUG: Cache never cleaned up
end
end
end
private
def expensive_computation(item)
# Simulate expensive operation
Array.new(1000) { "processed_#{item}_#{rand}" }
end
end
# Memory-safe alternative
class SafeProcessor
def initialize(cache_size_limit: 100)
@cache = {}
@cache_size_limit = cache_size_limit
end
def process_safely(items)
items.each_with_index do |item, idx|
attempt = 0
cache_key = nil
begin
attempt += 1
cache_key = "#{idx}_#{attempt}"
# Limit cache size to prevent memory leaks
clean_cache if @cache.size >= @cache_size_limit
@cache[cache_key] = expensive_computation(item)
if item.to_s.include?('retry') && attempt < 3
redo
end
yield @cache[cache_key]
ensure
# Clean up cache entry after use
@cache.delete(cache_key) if cache_key
end
end
end
private
def clean_cache
# Remove oldest half of cache entries
keys_to_remove = @cache.keys.first(@cache.size / 2)
keys_to_remove.each { |key| @cache.delete(key) }
end
def expensive_computation(item)
Array.new(100) { "processed_#{item}_#{rand}" } # Smaller allocation
end
end
Common Pitfalls
Scope and Context Confusion
One of the most frequent mistakes involves misunderstanding the scope in which flow control keywords operate, leading to unexpected behavior or runtime errors.
# Pitfall: Break in wrong context
def demonstrate_break_scope_issues
puts "=== Break in Proc vs Lambda ==="
# This will raise LocalJumpError
my_proc = proc do |n|
break if n > 3 # ERROR: break from proc-closure
n * 2
end
# This works correctly
my_lambda = lambda do |n|
return if n > 3 # OK: return from lambda
n * 2
end
begin
[1, 2, 3, 4, 5].map(&my_proc)
rescue LocalJumpError => e
puts "LocalJumpError with proc: #{e.message}"
end
result = [1, 2, 3, 4, 5].map(&my_lambda)
puts "Lambda result: #{result.inspect}"
end
# Pitfall: Break in method expecting block return
def method_expecting_each_value
[1, 2, 3, 4, 5].each do |n|
puts "Processing #{n}"
if n == 3
break "stopping here" # each returns this value, not the array
end
n * 2
end
end
# Correct alternative
def method_with_proper_collection
result = []
[1, 2, 3, 4, 5].each do |n|
puts "Processing #{n}"
if n == 3
result << "stopping here"
break
end
result << n * 2
end
result
end
# Pitfall: Next in map creating unexpected nils
def demonstrate_next_in_map
# This creates unwanted nils
numbers = [1, 2, 3, 4, 5, 6]
doubled = numbers.map do |n|
next if n.even? # Returns nil for even numbers
n * 2
end
puts "With next: #{doubled.inspect}" # [2, nil, 6, nil, 10, nil]
# Better alternatives
doubled_compact = numbers.map do |n|
n.even? ? nil : n * 2
end.compact
puts "Compacted: #{doubled_compact.inspect}" # [2, 6, 10]
# Or use select first
doubled_filtered = numbers.reject(&:even?).map { |n| n * 2 }
puts "Pre-filtered: #{doubled_filtered.inspect}" # [2, 6, 10]
end
Infinite Loop Traps
Redo and retry can easily create infinite loops when exit conditions are not properly managed or when state isn't correctly updated.
# Dangerous pattern: Redo without proper state management
def infinite_redo_trap
counter = 0
[1, 2, 3].each do |n|
puts "Processing #{n}"
# BUG: counter never resets, condition never changes
if n == 2 && counter < 5
counter += 1
redo # This will loop forever if counter >= 5 already
end
counter = 0 # Reset happens too late
end
end
# Safe pattern: Proper state management
def safe_redo_with_state
[1, 2, 3].each_with_index do |n, idx|
iteration_counter = 0 # Reset for each element
begin
iteration_counter += 1
puts "Processing #{n}, iteration #{iteration_counter}"
# Simulate condition requiring redo
if n == 2 && iteration_counter == 1
raise "Temporary error"
end
puts "Successfully processed #{n}"
rescue => e
if iteration_counter < 3
puts "Retrying #{n}: #{e.message}"
redo
else
puts "Max attempts reached for #{n}, moving on"
next
end
end
end
end
# Pitfall: Retry without changing conditions
def infinite_retry_trap
attempts = 0
begin
attempts += 1
puts "Attempt #{attempts}"
# BUG: Condition never changes, will retry forever
raise "This will always fail" if true
rescue => e
retry if attempts < 5 # This check helps but doesn't fix the logic error
end
end
# Solution: Ensure conditions can change
def safe_retry_pattern
attempts = 0
max_attempts = 5
success_probability = 0.3 # Start with low probability
begin
attempts += 1
puts "Attempt #{attempts}, success probability: #{success_probability}"
# Improve chances with each attempt
success_probability += 0.2
# Simulate operation that might succeed
if rand > success_probability
raise "Random failure"
else
puts "Operation succeeded!"
return "Success"
end
rescue => e
if attempts < max_attempts
puts "Failed: #{e.message}, retrying..."
retry
else
raise "Failed after #{max_attempts} attempts"
end
end
end
Exception Handling Integration Issues
Combining flow control keywords with exception handling can lead to subtle bugs and unexpected behavior patterns.
# Pitfall: Break/next inside ensure blocks
def problematic_ensure_flow
[1, 2, 3, 4, 5].each do |n|
begin
puts "Processing #{n}"
raise "Error on #{n}" if n == 3
rescue => e
puts "Caught: #{e.message}"
next # Skip to next iteration
ensure
puts "Ensure block for #{n}"
# PITFALL: break/next here affects the iterator, not the begin block
break if n == 4 # This will exit the each loop, not just the begin block
end
puts "After begin block for #{n}"
end
end
# Correct approach: Handle flow control outside ensure
def correct_ensure_flow
[1, 2, 3, 4, 5].each do |n|
should_break = false
should_continue = false
begin
puts "Processing #{n}"
raise "Error on #{n}" if n == 3
rescue => e
puts "Caught: #{e.message}"
should_continue = true
ensure
puts "Ensure block for #{n}"
should_break = true if n == 4
end
next if should_continue
break if should_break
puts "After begin block for #{n}"
end
end
# Pitfall: Retry changing exception context
def retry_context_confusion
outer_variable = "initial"
begin
puts "Outer variable: #{outer_variable}"
begin
inner_variable = "inner_#{Time.now.to_i}"
puts "Inner variable: #{inner_variable}"
# Modify outer state
outer_variable = "modified_#{Time.now.to_i}"
raise "Inner error"
rescue => e
puts "Inner rescue: #{e.message}"
# PITFALL: retry restarts the inner begin, but outer_variable
# keeps its modified value from previous attempts
retry if outer_variable.count('_') < 3
raise "Escalated: #{e.message}"
end
rescue => e
puts "Outer rescue: #{e.message}"
puts "Final outer variable: #{outer_variable}"
end
end
Reference
Flow Control Keywords Summary
Keyword | Context | Behavior | Return Value | Scope |
---|---|---|---|---|
break |
Loops, Iterators | Exits immediately | Optional value (default: nil ) |
Innermost enclosing loop/iterator |
break val |
Loops, Iterators | Exits with value | val |
Innermost enclosing loop/iterator |
next |
Loops, Iterators | Skips to next iteration | nil to collection (in map , etc.) |
Current iteration only |
redo |
Loops, Iterators | Restarts current iteration | N/A | Current iteration |
retry |
begin /rescue blocks, methods |
Restarts from beginning | N/A | Enclosing begin block or method |
Iterator Method Behavior
Method | With break |
With next |
With redo |
Notes |
---|---|---|---|---|
each |
Returns break value | Continues iteration | Repeats current iteration | Most common usage |
map |
Returns break value | Adds nil to result |
Repeats current iteration | next creates nils |
select |
Returns break value | Skips element | Repeats current iteration | next excludes element |
reject |
Returns break value | Skips element | Repeats current iteration | Opposite of select |
find |
Returns break value | Continues search | Repeats current iteration | Stops on first match |
reduce |
Returns break value | Skips element | Repeats current iteration | Accumulator unchanged on next |
Exception Context Compatibility
Context | break |
next |
redo |
retry |
Notes |
---|---|---|---|---|---|
while /until loops |
✓ | ✓ | ✓ | ✗ | Traditional loops |
for loops |
✓ | ✓ | ✓ | ✗ | Ruby-style for loops |
loop method |
✓ | ✓ | ✓ | ✗ | Infinite loop construct |
Iterator blocks | ✓ | ✓ | ✓ | ✗ | each , map , etc. |
begin /rescue |
✗ | ✗ | ✗ | ✓ | Exception handling only |
Method body | ✗ | ✗ | ✗ | ✓ | When method has rescue |
Proc objects | ✗* | ✓ | ✓ | ✗ | *Raises LocalJumpError |
Lambda objects | ✓ | ✓ | ✓ | ✗ | More permissive than procs |
Common Error Types
Error | Cause | Solution |
---|---|---|
LocalJumpError |
break /next in wrong context |
Use proper loop/iterator context |
LocalJumpError |
break in Proc |
Use lambda or return instead |
Infinite loop | redo /retry without exit condition |
Add attempt counters and limits |
Stack overflow | Excessive retry calls |
Limit retry attempts |
Unexpected nil |
next in map operations |
Use select first or compact after |
Logic errors | Flow control in ensure blocks |
Handle flow control outside ensure |
Performance Characteristics
Pattern | Time Complexity | Memory Usage | Best For |
---|---|---|---|
Early break |
O(k) where k < n | O(1) additional | Finding first match |
next filtering |
O(n) | O(n) for collections | Conditional processing |
redo with counter |
O(k*m) where m = retries | O(1) additional | Error recovery |
Exponential backoff retry |
Varies | O(1) | Network operations |
Circuit breaker pattern | O(1) when open | O(1) | Cascading failure prevention |