{"record":{"id":"c39014c5b78a927a","repo":"dgraph-io/badger","slug":"errnomerge","errorCode":"errNoMerge","errorMessage":"No need for merge","messagePattern":"No need for merge","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"info","filePath":"merge.go","lineNumber":49,"sourceCode":"type MergeFunc func(existingVal, newVal []byte) []byte\n\n// GetMergeOperator creates a new MergeOperator for a given key and returns a\n// pointer to it. It also fires off a goroutine that performs a compaction using\n// the merge function that runs periodically, as specified by dur.\nfunc (db *DB) GetMergeOperator(key []byte,\n\tf MergeFunc, dur time.Duration) *MergeOperator {\n\top := &MergeOperator{\n\t\tf:      f,\n\t\tdb:     db,\n\t\tkey:    key,\n\t\tcloser: z.NewCloser(1),\n\t}\n\n\tgo op.runCompactions(dur)\n\treturn op\n}\n\nvar errNoMerge = stderrors.New(\"No need for merge\")\n\nfunc (op *MergeOperator) iterateAndMerge() (newVal []byte, latest uint64, err error) {\n\ttxn := op.db.NewTransaction(false)\n\tdefer txn.Discard()\n\topt := DefaultIteratorOptions\n\topt.AllVersions = true\n\tit := txn.NewKeyIterator(op.key, opt)\n\tdefer it.Close()\n\n\tvar numVersions int\n\tfor it.Rewind(); it.Valid(); it.Next() {\n\t\titem := it.Item()\n\t\tif item.IsDeletedOrExpired() {\n\t\t\tbreak\n\t\t}\n\t\tnumVersions++\n\t\tif numVersions == 1 {\n\t\t\t// This should be the newVal, considering this is the latest version.","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/dgraph-io/badger/blob/2a001d466f6b71a917319a1db41f99860e16e269/merge.go#L31-L67","documentation":"Sentinel error of the MergeOperator (db.GetMergeOperator). iterateAndMerge computes a new value by folding the operator's merge function over all versions of a key; errNoMerge signals that there was exactly one version and nothing to merge, so the operator loop treats it as success (no compaction run needed). It is internal control flow, not a real failure — returned by Get via iterateAndMerge/compact paths and filtered out by the operator.","triggerScenarios":"Calling MergeOperator.Get (or the internal iterateAndMerge during periodic compaction) on a key that currently has exactly one version; nothing is wrong — the operator converts it to a nil error.","commonSituations":"Seen when wrapping operator internals or reading badger source/traces; users directly running iterateAndMerge-like logic or inspecting logs during low-contention periods where keys have single versions.","solutions":["Nothing to fix: if you observe errNoMerge from MergeOperator.Get results, it is consumed internally and means the value is the single stored version","If implementing custom merge logic, treat single-version keys as 'no merge required' and return the value as-is","Do not propagate errNoMerge (== errNoMerge) to callers; compare with errors.Is and map it to success"],"exampleFix":"// before\nval, _, err := op.iterateAndMerge()\nreturn err // leaks sentinel to callers\n// after\nval, _, err := op.iterateAndMerge()\nif err == ErrKeyNotFound || err == errNoMerge {\n    return nil\n}","handlingStrategy":"type-guard","validationCode":"val, err := op.Get()\nif errors.Is(err, errNoMerge) { err = nil }","typeGuard":"func isNoMerge(err error) bool { return err != nil && err.Error() == \"No need for merge\" }","tryCatchPattern":"val, err := op.Get()\nif err != nil && !isNoMerge(err) {\n    return err // treat errNoMerge as success\n}","preventionTips":["Consume MergeOperator APIs (Get/Merge) instead of reimplementing iterateAndMerge","When comparing errors, use errors.Is so internal sentinels are handled intentionally","Understand that single-version keys legitimately produce this sentinel"],"tags":["badger","merge-operator","sentinel-error","internal"],"backgroundTag":"sentinel-control-flow-error","analyzedSha":"2a001d466f6b71a917319a1db41f99860e16e269","analyzedAt":"2026-09-05T13:00:02.264Z","contentChangedAt":"2026-09-05T13:00:02.264Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}