Errors¶
Every error this module returns wraps one of the sentinel values below, so errors.Is
works on all of them:
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).
Related¶
- 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