Skip to content

Errors

Every error this module returns wraps one of the sentinel values below, so errors.Is works on all of them:

if errors.Is(err, config.ErrConflict) {
    // the file changed under us: re-read and retry
}

The tables give the message prefix each value carries, so an error you have in a log can be matched back to a name.

Which errors you must handle

Most callers need these four:

Situation Error What to do
Building a store ErrInvalidConfig The store is still usable. Fail fast in a service; carry on in a repair tool.
Writing ErrConflict Something else changed the file. Reload and re-plan.
Writing ErrSensitiveLeak You are about to write a secret into a plain file. Do not retry; fix the target.
Writing ErrForbiddenKey The source is not permitted to hold this key. Do not retry; write it somewhere else.

Everything else is either a programming mistake caught early, or an environment failure you would report rather than recover from.

Construction and validation

ErrNoSources

config: no sources configured

Returned by NewStore when no backend was supplied. A store with nothing to read has nothing to serve, so it is refused rather than returned empty. Add at least one of WithFiles, WithReaders, WithEnv, WithFlags or WithBackend.

ErrInvalidConfig

config: configuration is not valid

Returned when a candidate configuration fails schema validation. It comes from four places:

  • NewStore, when the first load parses but violates the schema. The store is returned alongside the error and is usable, because a tool whose job is to repair configuration needs something to repair it through.
  • Store.Reload, when a reload fails validation. The previous configuration stays live.
  • Store.Apply, when a change would introduce a violation. Violations that were already there do not block the write and are reported separately as pre-existing.
  • ValidateStruct[T], which wraps whatever the schema objected to.

A service should fail fast on it. A repair tool checks for it specifically:

store, err := config.NewStore(ctx, opts...)
if err != nil && !errors.Is(err, config.ErrInvalidConfig) {
    return err // genuinely unusable: no sources, unreadable, or unparseable
}

ErrEmptySchema

config: schema has no fields defined

Returned by NewSchema and SchemaOf[T] when tag parsing produced no fields, usually because the struct's fields carry no config tag, or carry config:"-". A schema that constrains nothing would validate everything, so it is refused rather than accepted as a no-op. See Struct tags.

ErrNoMergeFunc

config: section defaults require a merge function

Returned when a typed section supplies defaults but no way to combine them with the configured values, and the section exists in the configuration. Silently preferring one over the other would drop half the settings without saying so. Pass a merge function to WithSectionDefaults or WithSectionDefaultFunc.

Reading and decoding

ErrAmbiguousEnvKey

config: environment variable is ambiguous

Returned during load when a set environment variable could designate more than one existing configuration key, such as MYTOOL_A_B_C where both a.b_c and a_b.c exist. In a case-sensitive store, Log.Level beside log.level makes MYTOOL_LOG_LEVEL ambiguous in the same way. The message names both candidates and the variable.

Honouring it would mean guessing, and the guess would vary between runs of the same program. Rename one of the keys, or set the value in a file instead. See Environment variables.

ErrInvalidTarget

config: invalid target

One error value covering several "you cannot ask that of this" cases:

Where Cause
View.Unmarshal, View.UnmarshalKey, Value[T] The decode target is nil, is not a pointer, or is a nil pointer.
Store.AddLayer The layer was given no name; a layer needs one to appear in provenance.
Store.Apply, Store.Plan with To(…) The pinned name matches no writable source.
A nested store's write path The edit named a layer the aggregate does not carry.
NewStore, Store.AddLayer Two sources share an ID, so provenance could not tell them apart.
A write to a file The edit's path is empty, or it addresses a later document of a file that does not exist yet.

Store.WritableTargets() lists exactly the names To accepts.

Writing and routing

ErrNoChanges

config: no changes to apply

Returned by Store.Plan and Store.Apply when called with no changes. Guard the call site rather than treating it as a failure.

ErrInvalidPath

config: invalid path

Returned when a change names a malformed dotted path: blank, or with an empty segment (a..b, .a, a.). See Keys and paths.

Reads do not return this. An unresolvable path reads as the zero value.

ErrNoWritableLayer

config: no writable layer for change

Returned when a change has nowhere to go: every layer that could hold it is read-only. A store built only from WithReaders, WithEnv and WithFlags can never accept a write. Add a file backend, or a backend that implements WritableBackend.

ErrNotWritable

config: backend is not writable

Returned during Apply when a change was routed at a backend that cannot persist. Where ErrNoWritableLayer means "nowhere at all", this means "the chosen place refused". It is most often a pinned target inside a nested store that was not promoted with NestedPromotable.

ErrSensitiveLeak

config: refusing to write a sensitive key into a non-sensitive layer

Returned when a write would land a key that a Sensitive backend defines into a layer that is not itself sensitive, which would write secret-category material into a plain file.

Secrets backends are read-only, so a write to a key one of them owns routes down to the next writable layer. Refusing it is what keeps that safe. It also fires for a key a sensitive backend holds but a filter is hiding, because a deny list must not quietly turn a secret into a plaintext write.

Pinning the target does not opt out. This is a safety invariant, not a routing preference. A removal is exempt, because a removal writes no value and so cannot leak one.

ErrForbiddenKey

config: forbidden key

Returned when a write targets a key the source's Constrained policy forbids, such as a credential aimed at a project config file. The full message names both the key and the source that refused it.

It is its own sentinel rather than an ErrInvalidConfig, because the configuration is not invalid: the write was refused. A caller acts on the two differently: one is repaired by fixing a value, the other by writing somewhere else. That is the same distinction ErrSensitiveLeak draws.

The read side of the same policy is not an error at all. A forbidden key a source supplies stays visible and is reported through validation, so the leak is on the record rather than silently dropped.

ErrConflict

config: source changed since it was read

Returned when a source changed between being read and being written, so committing would silently discard someone else's work. Detection is by content hash rather than modification time, because timestamp granularity varies and is coarse on some filesystems.

This is detection, not prevention, which is the most that is portably achievable without lock files. Reload, re-plan against the new state, and retry.

ErrPartialCommit

config: commit partially applied

Returned when a write spanning several sources failed partway through and the already committed parts could not be fully rolled back. The message always names what is in which state; a caller is never left guessing.

There is no automatic recovery. Read the message, inspect the named sources, and reconcile by hand.

ErrWriteFromObserver

config: cannot change configuration from inside an observer

Returned by Store.Apply, Store.AddLayer and Store.Reload when called from inside an observer callback. Each such write is itself a change, which notifies, which runs the observer again, in a cascade with no natural end.

Take the change out of the observation: record what is needed, return, and write from somewhere else, such as a worker goroutine, a queue or the next tick. Serialising and de-duplicating those deferred writes belongs to you, because you are the only party that knows which of them still matter.

Backends, files and watching

ErrBackendParse

config: source could not be parsed

Returned when a source is not valid for its format. The message names the file and, for a multi-document source, the document index. It surfaces from NewStore, from Store.Reload and from a write that re-reads the file.

A reload that hits it is rejected outright and the last known good configuration stays live.

ErrBackendUnsafe

config: source cannot be safely edited: bad.yaml: 2:13 alias has no prior anchor in this document

Returned at load when a source parses as YAML but does not mean anything under its schema, so editing it would be refused: an alias that names no anchor, or a mapping with two keys that are the same value spelt differently. Every problem is listed with its line and column, separated by ; when there are several.

NewStore refuses such a source up front, naming the file and each problem, rather than letting you discover it at commit time with nowhere to put your edits. Fix the alias or the duplicate. See What survives a write.

ErrKeyCollision

config: two keys in one layer are the same key: app.yaml defines both "Port" and "port"

Returned when one source holds two keys that become the same key once the store lower-cases them, at any depth, or two non-string keys that read the same as text, such as 1 and 1.0. A store built with WithCaseSensitiveKeys lower-cases nothing, so there only the second kind collides. The source is refused whatever the two values are, because keeping either would be a silent choice of which setting you meant. Two different sources spelling a key differently is not a collision: the later source wins, as for any key.

NewStore returns it with no store. A reload, including one a watch triggered, is rejected and the last known good configuration stays live. Store.AddLayer withdraws the layer it added, and a write whose result would collide commits nothing. Rename or remove one of the two keys in the source.

ErrReadOnlyFS

config: filesystem is read-only

Returned by the write methods (WriteFile, Rename, Remove and MkdirAll) of a filesystem adapter over an inherently read-only source, such as an io/fs.FS or an HTTP URL. Nothing in this module returns it; the adapter modules do.

It exists so a caller can errors.Is it, rather than getting a bare fs.ErrPermission that a genuine permission failure is indistinguishable from.

ErrWatchUnavailable

config: cannot watch sources

Returned by Store.Watch when watching cannot work for the sources given: no watchable backend, or a polling watcher handed nothing to watch.

It is deliberately loud. A watcher that silently does nothing is worse than none: the application believes it will hear about changes and never will.

ErrStoreClosed

config: store is closed

Returned by Store.Reload, Store.Apply, Store.AddLayer and Store.Watch after Store.Close. A closed store still answers reads from the configuration it last held; it refuses anything that would load, write or watch, because what it reads through may already have been released. The refusal is never published to OnReloadError, since it is your own request being declined rather than a source failing.

An outer store whose nested inner store has been closed reports it too, through a failed reload and OnReloadError, and keeps its last good configuration. Rebuild the outer store without the closed one.

ErrCyclicStore

config: store is already present in this graph

Returned when composing stores would put one store into the graph twice, whether or not that closes a cycle. Two copies of the same store would contribute their layers at two precedences, so every value would be shadowed by a copy of itself.

Enforcement is by store identity alone, which leaves no room for a reachability analysis to be subtly wrong.

ErrKeyCaseMismatch

config: backend compares keys in the other case mode: nested store "team" is folded, this store is case-sensitive

Returned by NewStore when a backend cannot take the store's key mode. A nested store's keys were already folded, or not, by the store it wraps, so it must be built in the same mode as the store it is nested in. A backend that matches keys, such as a Filtered or Constrained one, takes the mode of the first store that owns it, and a store of the other mode is refused rather than left matching in the wrong one. Build both stores in the same mode, or give each its own backend.

ErrDuplicateLayer

config: two layers in the composed store are indistinguishable

Returned when two layers in a composed store are equal in kind, name and document, so nothing downstream can tell them apart. The observable symptom it prevents is a wrong plan: a key defined only in the hidden copy is invisible to shadow detection, so a write is reported as effective when it is not.

Two stores pointed at the same file is a composition mistake. Point one of them somewhere else.

ErrInternal

config: internal invariant violated

An invariant of this module does not hold. It is never the caller's fault and is always worth reporting. Open an issue with the message and, if you can, the configuration that produced it.

Errors the adapters return

Adapter modules define their own sentinels, in the adapter's package rather than this one. They answer the question "what did you not tell me", and they come back from the constructor, before any network call. Each is matched with errors.Is against the adapter's own value, for example errors.Is(err, configvault.ErrNoAddress). The ambient rung refuses a missing target rather than guessing one.

Error Module Returned when
ErrNoConfig config-consul, config-vault FromConfig is given a nil config.
ErrNoAddress config-vault FromConfig is given a config that names no address. Set Address, or VAULT_ADDR.
ErrNoEndpoints config-etcd FromConfig is given no endpoints, or Default finds none in ETCDCTL_ENDPOINTS. Unlike etcdctl, it does not fall back to localhost:2379.
ErrMalformedEnv config-etcd Default reads an ETCDCTL_* variable it cannot use: a bad dial timeout, TLS files that will not load, a certificate without its key, or a user without a password. The message names the variable.
ErrNoVaultURL config-azure-keyvault A rung is called with no vault URL.
ErrNoServiceURL config-azure-blob A rung is called with no storage-account URL.
ErrNoContainer config-azure-blob A rung is called with no container name.
ErrNoLocation config-gcp-parameter A rung is called with no location. Pass "global" unless the parameters are regionalised.

Other refusals follow the same pattern: ErrNoRegion (config-aws-ssm, config-aws-secrets, config-aws-s3), ErrNoProject (config-gcp-secret, config-gcp-parameter), ErrNoBucket (config-aws-s3, config-gcp-gcs), ErrNoEndpoint (config-azure-appconfig), ErrNoConn (config-sftp), ErrNoCredential (all three Azure adapters) and ErrNoConnectionString (config-azure-appconfig, config-azure-blob).

  • Write configuration: the write errors in the context of a task
  • Hot-reload safety: why a rejected reload changes nothing, and why writing from an observer is refused
  • Limitations: what the module will not do, of which several of these errors are the enforcement