temporalio/temporal · error

Can't deep copy non-zero time.Time: %v

Error message

Can't deep copy non-zero time.Time: %v

What it means

The dynamicconfig deep-copy machinery (used to isolate config values before mapstructure decoding) can only deep-copy zero time.Time values, since time.Time contains unexported fields. Copying a non-zero time.Time is impossible with this reflection approach, so it panics. The nolint note says this is expected to trigger only from static initializers, never production traffic.

Source

Thrown at common/dynamicconfig/deepcopy.go:61

		return deepCopyValue(v.Elem()).Addr()
	case reflect.Slice:
		if v.IsNil() {
			return v
		}
		nv := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
		for i := range v.Len() {
			nv.Index(i).Set(deepCopyValue(v.Index(i)))
		}
		return nv
	case reflect.Struct:
		// Special case for time.Time: it has unexported fields so we can't copy it field by
		// field, but we can copy zero values (which is all we need for default values).
		if v.Type() == reflect.TypeFor[time.Time]() {
			if v.Interface().(time.Time).IsZero() {
				return reflect.ValueOf(time.Time{})
			}
			// nolint:forbidigo // this will be triggered from a static initializer before it can be triggered from production code
			panic(fmt.Sprintf("Can't deep copy non-zero time.Time: %v", v.Interface()))
		}
		nv := reflect.New(v.Type()).Elem()
		for i := range v.Type().NumField() {
			nv.Field(i).Set(deepCopyValue(v.Field(i)))
		}
		return nv
	case reflect.Interface, reflect.Func, reflect.Chan:
		// only nil values of any other reference types allowed!
		if v.IsNil() {
			return v
		}
		fallthrough
	default:
		// nolint:forbidigo // this will be triggered from a static initializer before it can be triggered from production code
		panic(fmt.Sprintf("Can't deep copy value of type %s: %v", v.Type(), v))
	}
}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Change the default value to the zero time.Time{} and populate the timestamp at read/usage time
  2. Use a different type (e.g. unix seconds/milliseconds int64 or a duration) for time-like config fields
  3. Set the field only after decoding the config, not in the static default

Example fix

// before
var cfg = NewDefaultSetting(..., Setting{Deadline: time.Now()})
// after
var cfg = NewDefaultSetting(..., Setting{Deadline: time.Time{}}) // set real value after decode
Defensive patterns

Strategy: validation

Validate before calling

// ensure default structs contain only zero time.Time

Prevention

When it happens

Trigger: Declaring a dynamic config setting whose default value (or struct field inside a default struct) contains a non-zero time.Time, evaluated during package init when deepCopyForMapstructure/deepCopyValue runs.

Common situations: A developer adds a default like `time.Now()` or a populated timestamp to a config struct used as a dynamicconfig default; a struct default gains a new time.Time field that is initialized non-zero.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/8985115454b9fdb6. Report an issue: GitHub.