travisjeffery/timecop · error · SafeModeException
Safe mode is enabled, only calls passing a block are allowed
Error message
Safe mode is enabled, only calls passing a block are allowed.
What it means
Timecop::SafeModeException is raised when the timecop gem runs in safe mode and Timecop.freeze, Timecop.travel, or Timecop.scale is called WITHOUT a block. Safe mode (Timecop.safe_mode = true) is an opt-in policy that forces block-scoped time mocking so mocked time is always restored; the block path in Timecop#travel (lib/timecop/timecop.rb:221-230) pops the stack in an ensure clause. The guard at lib/timecop/timecop.rb:214 fires only when all three conditions hold: safe mode is on, no block is given, and the internal @safe flag is not set. @safe is set to true while a block is executing, so nested blockless calls made from inside a block are permitted.
Source
Thrown at lib/timecop/timecop.rb:214
end
def initialize #:nodoc:
@stack = []
@safe = nil
@thread_safe = false
end
def thread_safe=(t)
initialize
@thread_safe = t
end
def thread_safe
@thread_safe
end
def travel(mock_type, *args, &block) #:nodoc:
raise SafeModeException if Timecop.safe_mode? && !block_given? && !@safe
stack_item = TimeStackItem.new(mock_type, *args)
stack_backup = stack.dup
stack << stack_item
if block_given?
safe_backup = @safe
@safe = true
begin
yield stack_item.time
ensure
stack.replace stack_backup
@safe = safe_backup
end
end
end
View on GitHub (pinned to b3c9a5a6f0)
Solutions
- Pass a block so time is restored automatically: Timecop.freeze(new_time) do ... end (same for travel and scale).
- If you mock time across hooks, move the freeze into each example body inside a block; safe mode exists precisely to forbid the before/after blockless pattern.
- If blockless usage is intentional (whole-suite mocked time), disable the policy: set Timecop.safe_mode = false or remove the Timecop.safe_mode = true line from your test bootstrap.
- Locate where safe mode is enabled: grep -r 'safe_mode' spec/ test/ config/ lib/ and decide whether the policy or the call sites change.
- When wrapping Timecop in helpers or around-hooks, accept and forward &block explicitly so nested calls keep the protection of the enclosing block (@safe is true only during block execution).
Example fix
// before Timecop.safe_mode = true Timecop.freeze(Time.local(1990)) # => Timecop::SafeModeException: Safe mode is enabled, only calls passing a block are allowed. // after Timecop.safe_mode = true Timecop.freeze(Time.local(1990)) do # Time.now / Date.today / DateTime.now mocked only inside this block, restored on exit assert invoice.overdue? end
Defensive patterns
Strategy: validation
Validate before calling
# Guard before any blockless Timecop call if Timecop.safe_mode? raise ArgumentError, "Timecop is in safe mode: pass a block to freeze/travel/scale" end Timecop.freeze(target_time)
Type guard
# Ruby predicate helpers for wrapper methods def blockless_timecop_allowed? !Timecop.safe_mode? end def with_frozen_time(time, &block) raise Timecop::SafeModeException if Timecop.safe_mode? && block.nil? Timecop.freeze(time, &block) end
Try / catch
begin
Timecop.freeze(new_time)
rescue Timecop::SafeModeException
# policy violation, not a transient failure: switch to block form or disable safe mode
Timecop.freeze(new_time) { yield }
end
# Rescue the specific class only; never blanket-rescue StandardError for this error. Prevention
- Always call freeze/travel/scale with a block: the ensure clause restores real time even when the block raises, keeping tests order-independent.
- Centralize time mocking in one helper (e.g. a travel_to wrapper) so the safe-mode contract is enforced in a single place.
- Comment or log where Timecop.safe_mode = true is set in your test bootstrap so teammates can grep it before adding blockless calls.
- When wrapping Timecop in helpers or around-hooks, accept and forward &block explicitly — dropping it silently turns a legal nested call into a SafeModeException.
- Pair any before/after freeze pattern with after { Timecop.return } so one failing spec cannot leak mocked time into the next (the exact leak safe mode protects against).
When it happens
Trigger: 1) Timecop.safe_mode = true is active (spec_helper.rb, rails_helper.rb, or a support file) and any blockless call is made: Timecop.freeze, Timecop.freeze(Time.local(1990)), Timecop.travel(2008, 9, 1), Timecop.scale(3600) — freeze/travel/scale all route through send_travel -> instance.travel, so all three are guarded. 2) A before/after hook pair (Timecop.freeze(t) in before, Timecop.return in after — the README setup/teardown pattern) runs while safe mode is enabled. 3) A Rails config/environments/test.rb after_initialize hook calls Timecop.travel(t) blockless (the exact README recipe) while safe mode was switched on elsewhere. 4) A wrapper/helper method drops the caller's block by not accepting and forwarding &block, silently turning a legal call into a blockless one.
Common situations: Safe mode enabled in shared test bootstrap (spec/support, a copied boilerplate file, or a company test-tooling gem) by another team member, after which legacy blockless Timecop calls fail suite-wide. Refactoring a block call into a before/after pair without realizing safe mode forbids exactly that pattern. Adopting a shared gem or CI config that flips Timecop.safe_mode globally. Defining helper methods like def freeze_time(time); Timecop.freeze(time); end that never forward a block.
AI-assisted analysis of travisjeffery/timecop@b3c9a5a6f0 (2026-08-23).
Data as JSON: /api/errors/6944376a8e116fe5.
Report an issue: GitHub.