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

  1. Pass a block so time is restored automatically: Timecop.freeze(new_time) do ... end (same for travel and scale).
  2. 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.
  3. 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.
  4. Locate where safe mode is enabled: grep -r 'safe_mode' spec/ test/ config/ lib/ and decide whether the policy or the call sites change.
  5. 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

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.