temporalio/temporal · error
Can't deep copy non-zero time.Time
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.
Solutions
- Change the default value to the zero time.Time{} and populate the timestamp at read/usage time
- Use a different type (e.g. unix seconds/milliseconds int64 or a duration) for time-like config fields
- 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
- Zero-value time.Time in defaults
- Use durations/epoch ints instead
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
- Can't deep copy value of type
- ConvertGradualChange can only be used with scalar types for…
- duplicate registration of dynamic config key
- dynamicconfig.New*Setting must only be called from static…
- unsupported error type
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/8985115454b9fdb6.
Report an issue: GitHub.
Appendix: 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)