etcd-io/etcd · error

unsupported stm

Error message

unsupported stm

What it means

concurrency.NewSTM panics with "unsupported stm" in mkSTM when the configured Isolation value does not match any of the four supported levels: SerializableSnapshot, Serializable, RepeatableReads, ReadCommitted. Since Isolation is an unexported-constant int type, any value outside 0-3 — typically produced by casting an arbitrary integer or defining a custom level — hits the default branch.

Source

Thrown at client/v3/concurrency/stm.go:132

		}
		return s
	case Serializable:
		s := &stmSerializable{
			stm:      stm{client: c, ctx: opts.ctx},
			prefetch: make(map[string]*v3.GetResponse),
		}
		s.conflicts = func() []v3.Cmp { return s.rset.cmps() }
		return s
	case RepeatableReads:
		s := &stm{client: c, ctx: opts.ctx, getOpts: []v3.OpOption{v3.WithSerializable()}}
		s.conflicts = func() []v3.Cmp { return s.rset.cmps() }
		return s
	case ReadCommitted:
		s := &stm{client: c, ctx: opts.ctx, getOpts: []v3.OpOption{v3.WithSerializable()}}
		s.conflicts = func() []v3.Cmp { return nil }
		return s
	default:
		panic("unsupported stm")
	}
}

type stmResponse struct {
	resp *v3.TxnResponse
	err  error
}

func runSTM(s STM, apply func(STM) error) (*v3.TxnResponse, error) {
	outc := make(chan stmResponse, 1)
	go func() {
		defer func() {
			if r := recover(); r != nil {
				e, ok := r.(stmError)
				if !ok {
					// client apply panicked
					panic(r)
				}

View on GitHub (pinned to f744d457f4)

Solutions

  1. Pass only the exported constants: concurrency.WithIsolation(concurrency.SerializableSnapshot) (or Serializable, RepeatableReads, ReadCommitted).
  2. If the level comes from configuration, validate it maps to one of the four constants before calling NewSTM; reject unknown values with a normal error.
  3. Omit WithIsolation entirely to get the default SerializableSnapshot.

Example fix

// before
iso := concurrency.Isolation(cfg.IsolationLevelInt) // cfg says 9
concurrency.NewSTM(cli, apply, concurrency.WithIsolation(iso)) // panics: unsupported stm

// after
levels := map[string]concurrency.Isolation{
    "serializable-snapshot": concurrency.SerializableSnapshot,
    "serializable": concurrency.Serializable,
    "repeatable-reads": concurrency.RepeatableReads,
    "read-committed": concurrency.ReadCommitted,
}
iso, ok := levels[cfg.IsolationLevel]
if !ok {
    return fmt.Errorf("unknown isolation level %q", cfg.IsolationLevel)
}
concurrency.NewSTM(cli, apply, concurrency.WithIsolation(iso))
Defensive patterns

Strategy: validation

Validate before calling

func parseIsolation(s string) (concurrency.Isolation, error) {
	switch s {
	case "serializable-snapshot", "":
		return concurrency.SerializableSnapshot, nil
	case "serializable":
		return concurrency.Serializable, nil
	case "repeatable-reads":
		return concurrency.RepeatableReads, nil
	case "read-committed":
		return concurrency.ReadCommitted, nil
	default:
		return 0, fmt.Errorf("unknown isolation level %q", s)
	}
}

Type guard

func isValidIsolation(l concurrency.Isolation) bool {
	return l >= concurrency.SerializableSnapshot && l <= concurrency.ReadCommitted
}

Prevention

When it happens

Trigger: concurrency.NewSTM(cli, apply, concurrency.WithIsolation(concurrency.Isolation(7))) or Isolation(-1), or reading the level from a config file/int flag and casting it directly. Constructing stmOptions manually with an unset-but-nonzero iso field is another path.

Common situations: Mapping a user-configurable isolation string/number to the Isolation type without validating the range; refactoring that renumbers or removes constants; copy-pasting a level constant from a different etcd version or another library.

Related errors


AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15). Data as JSON: /api/errors/d02fb378f989b291. Report an issue: GitHub.