1# Configuration in Effect
2
3This guide shows you how to load and validate configuration in an Effect application. Two modules work together:
4
5- **`ConfigProvider`** — reads raw data from a source (environment variables, JSON objects, `.env` files, directory trees).
6- **`Config`** — describes what shape and types you expect, then decodes the raw data into typed values.
7
8You describe _what_ you need with `Config`, and the library figures out _how_ to read and validate it using a `ConfigProvider`.
9
10## Getting Started
11
12### Reading a Single Value
13
14The simplest case: read one value from an environment variable.
15
16```ts
17import { Config, Effect } from "effect"
18
19const program = Effect.gen(function*() {
20 const host = yield* Config.String("HOST")
21 console.log(host)
22})
23
24Effect.runSync(program)
25// reads HOST from process.env
26```
27
28When you yield a `Config` inside `Effect.gen`, it automatically uses the default `ConfigProvider` (which reads from `process.env`).
29
30### Reading Multiple Values
31
32Use `Config.all` to group related keys:
33
34```ts
35import { Config, ConfigProvider, Effect } from "effect"
36
37const dbConfig = Config.all({
38 host: Config.String("host"),
39 port: Config.Int("port")
40})
41
42const provider = ConfigProvider.fromUnknown({
43 host: "localhost",
44 port: 5432
45})
46
47const result = Effect.runSync(dbConfig.parse(provider))
48// { host: "localhost", port: 5432 }
49```
50
51### Reading Structured Config with a Schema
52
53For larger configs, use `Config.schema` with a `Schema.Struct`:
54
55```ts
56import { Config, ConfigProvider, Effect, Schema } from "effect"
57
58const AppConfig = Config.schema(
59 Schema.Struct({
60 host: Schema.String,
61 port: Schema.Int,
62 debug: Schema.Boolean
63 })
64)
65
66const provider = ConfigProvider.fromUnknown({
67 host: "localhost",
68 port: 8080,
69 debug: true
70})
71
72const result = Effect.runSync(AppConfig.parse(provider))
73// { host: "localhost", port: 8080, debug: true }
74```
75
76The schema automatically decodes raw string values into their target types. For example, when reading from environment variables, `"8080"` becomes the number `8080` and `"true"` becomes the boolean `true`.
77
78## Config Constructors
79
80Each constructor reads and decodes a configuration value into the appropriate type.
81
82| Constructor | Decoded type | Notes |
83| -------------------------------- | ------------------ | ------------------------------------------------------------------------ |
84| `Config.String(name?)` | `string` | Any string |
85| `Config.NonEmptyString(name?)` | `string` | Rejects `""` |
86| `Config.Number(name?)` | `number` | Includes `NaN`, `Infinity` |
87| `Config.Finite(name?)` | `number` | Rejects `NaN` and `Infinity` |
88| `Config.Int(name?)` | `number` | Integers only |
89| `Config.Boolean(name?)` | `boolean` | Accepts `true/false`, `yes/no`, `on/off`, `1/0`, `y/n` |
90| `Config.Port(name?)` | `number` | Integer in 1–65535 |
91| `Config.URL(name?)` | `URL` | Parsed via the `URL` constructor |
92| `Config.Date(name?)` | `Date` | Rejects invalid dates |
93| `Config.Duration(name?)` | `Duration` | Parses `"10 seconds"`, `"500 millis"`, `"Infinity"`, `"-Infinity"`, etc. |
94| `Config.LogLevel(name?)` | `string` | One of `All`, `Fatal`, `Error`, `Warn`, `Info`, `Debug`, `Trace`, `None` |
95| `Config.Redacted(name?)` | `Redacted<string>` | Hidden from logs and `toString` |
96| `Config.Literal(value, name?)` | literal type | Accepts only the given literal |
97| `Config.Literals(values, name?)` | literal union | Accepts one of the given literals |
98| `Config.Array(value, ...)` | `ReadonlyArray<V>` | Accepts structural arrays and flat separated strings |
99| `Config.Record(key, value, ...)` | `Record<K, V>` | Accepts structural records and flat separated key-value strings |
100
101The optional `name` parameter sets the local path segment for lookup. If the config is wrapped with `Config.nested`, the nested prefix is prepended to this local path. Omit `name` when the config should decode the provider root. `Config.Array` and `Config.Record` additionally accept an options object directly when no path is needed, or a path followed by the options object.
102
103### Parsing and Path Ownership
104
105A `Config` exposes `parse(provider)`; lookup prefixes are not part of this public method. Build paths declaratively with the constructor's `name` / `path` argument and `Config.nested`.
106
107This keeps the two path responsibilities separate:
108
109- `Config.schema(..., path)` and `Config.nested(name)` describe the logical path of a setting.
110- `ConfigProvider.mapInput`, `ConfigProvider.nested`, and case-conversion combinators map logical paths to a source.
111
112The same rule applies when a `Config` is yielded as an `Effect`: the config uses the current `ConfigProvider`, while its internally composed logical path stays an implementation detail.
113
114## Config Combinators
115
116### `Config.withDefault` — Fallback for Absent Input
117
118Triggers when the config is absent. For `Config.all`, any absent child makes the group absent unless another child fails. A group default replaces the entire group; apply defaults to individual children to keep other supplied values. Validation and source errors still propagate.
119
120```ts
121import { Config, ConfigProvider, Effect } from "effect"
122
123const port = Config.Int("port").pipe(Config.withDefault(3000))
124
125const provider = ConfigProvider.fromUnknown({})
126Effect.runSync(port.parse(provider)) // 3000
127```
128
129### `Config.option` — Optional Values
130
131Returns `Option.some(value)` on success and `Option.none()` when the config is absent. A successful `undefined` value is still a success, so a schema that accepts missing input produces `Option.some(undefined)`, not `Option.none()`.
132
133```ts
134import { Config, ConfigProvider, Effect } from "effect"
135
136const maybePort = Config.option(Config.Int("port"))
137
138const provider = ConfigProvider.fromUnknown({})
139Effect.runSync(maybePort.parse(provider)) // { _tag: "None" }
140```
141
142### `Config.map` — Transform a Value
143
144```ts
145import { Config } from "effect"
146
147const upperHost = Config.String("HOST").pipe(
148 Config.map((s) => s.toUpperCase())
149)
150```
151
152### `Config.orElse` — Fallback on Any Error
153
154Unlike `withDefault`, this handles absence and **all** `ConfigError`s. The fallback's result replaces the original failure. If the fallback is absent, a later `withDefault` or `option` can recover it. If the fallback fails, only its error propagates:
155
156```ts
157import { Config } from "effect"
158
159const host = Config.String("HOST").pipe(
160 Config.orElse(() => Config.succeed("localhost"))
161)
162```
163
164### `Config.nested` — Scope a Config Under a Prefix
165
166Prepends a logical path segment to every key the inner config reads. The prefix is used for both provider lookups and schema error paths:
167
168```ts
169import { Config, ConfigProvider, Effect } from "effect"
170
171const dbConfig = Config.all({
172 host: Config.String("host"),
173 port: Config.Int("port")
174}).pipe(Config.nested("database"))
175
176const provider = ConfigProvider.fromUnknown({
177 database: { host: "localhost", port: 5432 }
178})
179
180Effect.runSync(dbConfig.parse(provider))
181// { host: "localhost", port: 5432 }
182```
183
184With environment variables, nesting uses `_` as separator:
185
186```ts
187import { Config, ConfigProvider, Effect } from "effect"
188
189const host = Config.String("host").pipe(Config.nested("database"))
190
191const provider = ConfigProvider.fromEnv({
192 env: { database_host: "localhost" }
193})
194
195Effect.runSync(host.parse(provider)) // "localhost"
196```
197
198Multiple `Config.nested` calls compose with the outermost prefix first:
199
200```ts
201import { Config, ConfigProvider, Effect } from "effect"
202
203const config = Config.String("host").pipe(
204 Config.nested("database"),
205 Config.nested("production")
206)
207
208const provider = ConfigProvider.fromUnknown({
209 production: {
210 database: {
211 host: "localhost"
212 }
213 }
214})
215
216Effect.runSync(config.parse(provider)) // "localhost"
217```
218
219### `Config.all` — Combine Multiple Configs
220
221Accepts a record, a tuple, or an iterable. A group is absent when any child is absent and no child fails. `Config.withDefault` replaces the entire absent group, and `Config.option` returns `None`. Validation and source errors propagate even when another child is absent.
222
223```ts
224import { Config } from "effect"
225
226// As a record
227const appConfig = Config.all({
228 host: Config.String("host"),
229 port: Config.Int("port"),
230 debug: Config.Boolean("debug")
231})
232
233// As a tuple
234const pair = Config.all([Config.String("a"), Config.Int("b")])
235```
236
237For example, providing only `host` selects the default for the entire group here:
238
239```ts
240import { Config } from "effect"
241
242const database = Config.all({
243 host: Config.String("host"),
244 port: Config.Int("port")
245}).pipe(
246 Config.withDefault({ host: "localhost", port: 5432 })
247)
248```
249
250If the provider contains `{ host: "db.internal" }`, the result is `{ host: "localhost", port: 5432 }`. To keep the supplied host and default only the port, put `Config.withDefault(5432)` on the `port` child instead.
251
252Child defaults resolve missing children before the group decides whether it is absent:
253
254```ts
255const listener = Config.all({
256 host: Config.String("host"),
257 port: Config.Int("port").pipe(Config.withDefault(8080))
258}).pipe(Config.option)
259```
260
261`listener` is `None` when `host` is absent, including when a valid `port` is present. It is `Some` when `host` is present and `port` resolves to either a supplied value or its default. An invalid `port` fails even if `host` is absent.
262
263### How Absence Is Decided
264
265Configuration evaluation distinguishes three situations before producing the public `Effect`:
266
2671. **Resolved** — decoding succeeded. The value may legitimately be `undefined`, `{}`, or `[]`.
2682. **Absent** — a schema could not resolve because its relevant provider representation was unavailable, or a combined config has an absent child and no failed children.
2693. **Failed** — the provider failed, input was invalid, or an effectful transformation failed.
270
271`Config.withDefault` and `Config.option` handle only the second case. `Config.orElse` handles both absence and failures and adopts the fallback's result. Once a failure is recovered, it does not influence enclosing groups or later defaults.
272
273At the lookup path of a `Config.schema`, an unavailable representation is passed to the schema decoder as `undefined`. This includes a missing node and a present node whose shape cannot represent the schema: for example, an array node cannot represent a struct. Missing properties inside an object remain omitted so the schema's property semantics still apply. The decoder runs before absence is decided. Consequently:
274
275- `Config.schema(Schema.UndefinedOr(Schema.String), "key")` succeeds with `undefined` when `key` is absent.
276- An explicitly present empty object can decode to `{}` when the schema permits it.
277- Wrapping either successful result in `Config.option` produces `Some`, because decoding succeeded.
278- If the schema rejects `undefined` and no relevant representation was found, `Config.withDefault` uses its fallback and `Config.option` returns `None`.
279- Present invalid data is a failure. A `Config.all` group with missing children is absent unless another child fails.
280- `SourceError` is always a failure and is never replaced by `withDefault` or `option`.
281
282`Config.schema(Schema.Struct(...))` and `Config.all(...)` describe different lookup models. A struct schema validates one structured input. An existing object, including an empty object, fails validation if required fields are missing. `Config.all` evaluates independent child configs and can recover a partially supplied group through `Config.withDefault` or `Config.option`. Field optionality in `Config.all` is expressed on each child with `Config.option` or `Config.withDefault`.
283
284### How Schema Input Is Loaded
285
286`Config.schema` converts its codec to the canonical `Schema.StringTree` codec and uses the encoded AST to decide which provider representation to load:
287
288- A scalar schema reads the node's scalar value. A record or array node may have a co-located scalar value in addition to its children.
289- A struct loads its declared properties and omits children that the provider does not contain. A record schema also loads advertised keys that match its index signature.
290- An array or tuple loads its indexed children. Missing positions are represented as `undefined` so the element schema decides whether they are valid.
291- A union whose members require different shapes materializes each member independently. Schema then applies the union's declared order or `oneOf` rule and any checks attached to the original union.
292
293This keeps the provider responsible only for reporting what exists. Schema remains responsible for deciding whether the loaded representation is valid.
294
295Plain `Schema.Array` and `Schema.Record` accept structural provider input only. Use `Config.Array(Schema.String, "items")` for separated scalar input such as `"a,b,c"`, and `Config.Record(Schema.String, Schema.String, "items")` for input such as `"a=1,b=2"`. Both constructors also accept structural input.
296
297The canonical `StringTree` encoding must expose a concrete scalar, object, array, or union shape. `Config.schema` rejects opaque encodings such as `Schema.Any`, `Schema.Unknown`, `Schema.ObjectKeyword`, `Schema.Json`, and `Schema.MutableJson` synchronously when the config is constructed, including when they are nested in another schema. Suspended recursive schemas and declarations such as `Schema.URL` remain supported when their eventual canonical encoding has a concrete shape. To read arbitrary JSON from one scalar provider value, use `Schema.fromJsonString(Schema.Json)`.
298
299### Custom Config Logic
300
301There is no public low-level `Config.make` constructor. For custom validation or transformation, start from one of the public constructors or `Config.schema`, then use `Config.map`, `Config.mapEffect`, `Config.all`, `Config.orElse`, or `Config.withDefault`.
302
303If you need custom lookup behavior for a new backing source, implement a `ConfigProvider` with `ConfigProvider.make` instead.
304
305## Array and Record Constructors
306
307`Config.Array` accepts `{ separator? }`, while `Config.Record` accepts `{ separator?, keyValueSeparator? }`. Pass the options object directly to read the provider root, or place a string or `ConfigProvider.Path` before it to select a path:
308
309```ts
310import { Config, Schema } from "effect"
311
312const rootValues = Config.Array(Schema.String, { separator: ";" })
313const namedValues = Config.Array(Schema.String, "VALUES", { separator: ";" })
314
315const rootHeaders = Config.Record(Schema.String, Schema.String, { keyValueSeparator: ":" })
316const namedHeaders = Config.Record(Schema.String, Schema.String, "HEADERS", { keyValueSeparator: ":" })
317```
318
319## ConfigProvider Sources
320
321The concrete built-in source providers `fromEnv`, `fromDotEnvContents`, `fromDotEnv`, `fromUnknown`, and `fromDir` treat literal empty strings as missing values by default when they are loaded as values. Container discovery still reflects the source structure, so a key or file can appear in a `Record` or `Array` node and then load as missing. Pass `{ preserveEmptyStrings: true }` to preserve empty strings as explicit values.
322
323At the raw provider interface, `load(path)` succeeds with `Node | undefined`: a
324`Node` means the path exists, while `undefined` means it does not. A
325`SourceError` represents a failure to read the source and remains in the Effect
326error channel.
327
328Lookup-level `undefined` is distinct from the `value` field of a found `Record`
329or `Array` node. Such a container can exist while
330`node.value === undefined`, which means that it has children but no co-located
331scalar value.
332
333### `ConfigProvider.fromEnv` — Environment Variables (Default)
334
335This is the default provider. Path segments are joined with `_` for lookup.
336
337```ts
338import { Config, ConfigProvider, Effect } from "effect"
339
340const provider = ConfigProvider.fromEnv({
341 env: {
342 DATABASE_HOST: "localhost",
343 DATABASE_PORT: "5432"
344 }
345})
346
347const host = Config.String("HOST").parse(
348 provider.pipe(ConfigProvider.nested("DATABASE"))
349)
350
351Effect.runSync(host) // "localhost"
352```
353
354**How `_` splitting works**: env var names are split on `_` to build a tree. This means `DATABASE_HOST=localhost` is accessible at both `["DATABASE_HOST"]` (flat) and `["DATABASE", "HOST"]` (nested). Querying `["DATABASE"]` returns a Record node with child key `"HOST"`.
355
356Pass `{ env: { ... } }` for testing. Omit to use `process.env` (merged with `import.meta.env` when available).
357
358### `ConfigProvider.fromUnknown` — Plain JS Objects
359
360Ideal for testing or embedding config in code:
361
362```ts
363import { Config, ConfigProvider, Effect } from "effect"
364
365const provider = ConfigProvider.fromUnknown({
366 database: {
367 host: "localhost",
368 port: 5432,
369 credentials: {
370 username: "admin",
371 password: "secret"
372 }
373 },
374 servers: ["server1", "server2", "server3"]
375})
376```
377
378Path traversal follows standard JS rules: string segments index into object keys, numeric segments index into arrays. Primitive values are automatically stringified.
379
380### `ConfigProvider.fromDotEnvContents` — Parse `.env` Strings
381
382When you already have the `.env` content as a string:
383
384```ts
385import { ConfigProvider } from "effect"
386
387const contents = `
388# Database settings
389HOST=localhost
390PORT=3000
391SECRET="my-secret-value"
392`
393
394const provider = ConfigProvider.fromDotEnvContents(contents)
395```
396
397Supports `export` prefixes, single/double/backtick quoting, inline comments, and escaped newlines. Enable variable expansion with `{ expandVariables: true }`:
398
399```ts
400import { ConfigProvider } from "effect"
401
402const contents = `
403PASSWORD=secret
404DB_PASS=$PASSWORD
405`
406
407const provider = ConfigProvider.fromDotEnvContents(contents, {
408 expandVariables: true
409})
410```
411
412### `ConfigProvider.fromDotEnv` — Load `.env` Files
413
414Reads a `.env` file from disk. Returns an `Effect` (requires `FileSystem` in context):
415
416```ts
417import { ConfigProvider, Effect } from "effect"
418
419const program = Effect.gen(function*() {
420 const provider = yield* ConfigProvider.fromDotEnv()
421 // or: yield* ConfigProvider.fromDotEnv({ path: "/custom/.env" })
422 return provider
423})
424```
425
426### `ConfigProvider.fromDir` — Directory Trees
427
428Reads config from a file-system tree where each file is a leaf and each directory is a container. Useful for Kubernetes ConfigMap/Secret volume mounts.
429
430```
431/etc/myapp/
432 database/
433 host # contains "localhost"
434 port # contains "5432"
435 api_key # contains "sk-abc123"
436```
437
438```ts
439import { ConfigProvider, Effect } from "effect"
440
441const program = Effect.gen(function*() {
442 const provider = yield* ConfigProvider.fromDir({
443 rootPath: "/etc/myapp"
444 })
445 return provider
446})
447```
448
449Requires `Path` and `FileSystem` in the Effect context.
450
451Missing files and directories return `undefined`, so fallback providers can handle the path. Empty files also return `undefined` by default after trimming their contents, while directory listings still report the file names present on disk; pass `{ preserveEmptyStrings: true }` to preserve them as `Value("")`. Other file-system failures are reported as `SourceError`.
452
453### `ConfigProvider.make` — Custom Sources
454
455Build a provider from any backing store:
456
457```ts
458import { ConfigProvider, Effect } from "effect"
459
460const data: Record<string, string> = {
461 host: "localhost",
462 port: "5432"
463}
464
465const provider = ConfigProvider.make((path) => {
466 const key = path.join(".")
467 const value = data[key]
468 return Effect.succeed(
469 value !== undefined ? ConfigProvider.makeValue(value) : undefined
470 )
471})
472```
473
474Return `undefined` for "not found" and a `Node` for a path that exists. Only
475fail with `SourceError` when the source itself cannot be read. Providers created
476with `make` automatically support the path-transformation behavior used by
477`mapInput`, `constantCase`, and `nested`.
478
479## ConfigProvider Combinators
480
481### `ConfigProvider.orElse` — Fallback Sources
482
483Falls back to a second provider when the first returns `undefined` (path not found). Does **not** catch `SourceError`.
484
485```ts
486import { ConfigProvider } from "effect"
487
488const envProvider = ConfigProvider.fromEnv({
489 env: { HOST: "prod.example.com" }
490})
491const defaults = ConfigProvider.fromUnknown({
492 HOST: "localhost",
493 PORT: "3000"
494})
495
496const combined = ConfigProvider.orElse(envProvider, defaults)
497```
498
499Each side keeps its own path transformations. If you combine providers that were already scoped or mapped, those transformations remain local to that side:
500
501```ts
502import { ConfigProvider } from "effect"
503
504const envProvider = ConfigProvider.fromEnv({
505 env: { DATABASE_HOST: "localhost" }
506}).pipe(ConfigProvider.constantCase)
507
508const defaults = ConfigProvider.fromEnv({
509 env: { APP_PORT: "3000" }
510}).pipe(ConfigProvider.nested("APP"))
511
512const combined = envProvider.pipe(ConfigProvider.orElse(defaults))
513```
514
515### `ConfigProvider.nested` — Prefix All Lookups
516
517Prepends path segments so that all lookups are scoped:
518
519```ts
520import { ConfigProvider } from "effect"
521
522const provider = ConfigProvider.fromEnv({
523 env: { APP_HOST: "localhost", APP_PORT: "3000" }
524})
525
526// Lookups for ["HOST"] now resolve to ["APP", "HOST"]
527const scoped = ConfigProvider.nested(provider, "APP")
528```
529
530Accepts a single string or a full `Path` array.
531
532Provider transformations compose in application order. A later `nested` becomes the outer prefix:
533
534```ts
535import { ConfigProvider } from "effect"
536
537const provider = ConfigProvider.fromEnv({
538 env: { B_A_KEY: "value" }
539}).pipe(
540 ConfigProvider.nested("A"),
541 ConfigProvider.nested("B")
542)
543
544// path ["KEY"] resolves to ["B", "A", "KEY"]
545```
546
547When `nested` is applied to a provider built with `ConfigProvider.orElse`, the prefix is applied to both operands.
548
549### `ConfigProvider.constantCase` — CamelCase to SCREAMING_SNAKE_CASE
550
551Bridges camelCase schema keys to environment variable naming:
552
553```ts
554import { ConfigProvider } from "effect"
555
556const provider = ConfigProvider.fromEnv({
557 env: { DATABASE_HOST: "localhost" }
558}).pipe(ConfigProvider.constantCase)
559
560// path ["databaseHost"] now resolves to ["DATABASE_HOST"]
561```
562
563Ordering matters with `nested`. `constantCase` is a path transform, so it only converts the path it receives at that point in the pipeline:
564
565```ts
566import { ConfigProvider } from "effect"
567
568const convertedPrefix = ConfigProvider.fromEnv({
569 env: { APP_HOST: "localhost" }
570}).pipe(
571 ConfigProvider.nested("app"),
572 ConfigProvider.constantCase
573)
574
575// path ["host"] resolves to ["APP", "HOST"]
576
577const literalPrefix = ConfigProvider.fromEnv({
578 env: { app_HOST: "localhost" }
579}).pipe(
580 ConfigProvider.constantCase,
581 ConfigProvider.nested("app")
582)
583
584// path ["host"] resolves to ["app", "HOST"]
585```
586
587Put `constantCase` after `nested` when the prefix should be converted too.
588
589### `ConfigProvider.mapInput` — Arbitrary Path Transforms
590
591Transform the whole path before lookup:
592
593```ts
594import { ConfigProvider } from "effect"
595
596const provider = ConfigProvider.fromEnv({
597 env: { APP_HOST: "localhost" }
598})
599
600const upper = ConfigProvider.mapInput(
601 provider,
602 (path) => path.map((seg) => typeof seg === "string" ? seg.toUpperCase() : seg)
603)
604```
605
606Path transformation is a capability of the `ConfigProvider` interface. The
607exported `ConfigProvider.mapInput` combinator delegates to that capability,
608rather than passing an extra transformation argument to `load`. This keeps
609ordinary lookup fixed as `load(path)` and allows composite providers to
610preserve their own behavior without exposing representation state. Custom
611source providers should normally be constructed with `ConfigProvider.make`,
612which implements this capability automatically.
613
614`mapInput` runs after earlier provider transformations, so it sees the full path produced so far:
615
616```ts
617import { ConfigProvider } from "effect"
618
619const appendLeaf = ConfigProvider.mapInput((path) => [...path, "leaf"])
620
621const provider = ConfigProvider.fromEnv({
622 env: { APP_KEY_leaf: "value" }
623}).pipe(
624 ConfigProvider.nested("APP"),
625 appendLeaf
626)
627
628// path ["KEY"] resolves to ["APP", "KEY", "leaf"]
629```
630
631When `mapInput` is applied to a provider built with `ConfigProvider.orElse`, the mapping is applied to both operands.
632
633## Installing a Provider
634
635### Using `ConfigProvider.layer`
636
637Replaces the active provider for all downstream effects:
638
639```ts
640import { Config, ConfigProvider, Effect } from "effect"
641
642const TestLayer = ConfigProvider.layer(
643 ConfigProvider.fromUnknown({ port: 8080 })
644)
645
646const program = Effect.gen(function*() {
647 const port = yield* Config.Int("port")
648 return port
649})
650
651Effect.runSync(Effect.provide(program, TestLayer)) // 8080
652```
653
654### Using `ConfigProvider.layerAdd`
655
656Adds a provider without replacing the existing one. By default, the new provider is a **fallback**:
657
658```ts
659import { ConfigProvider } from "effect"
660
661const defaults = ConfigProvider.fromUnknown({
662 HOST: "localhost",
663 PORT: "3000"
664})
665
666// process.env is tried first; `defaults` is the fallback
667const DefaultsLayer = ConfigProvider.layerAdd(defaults)
668```
669
670Set `{ asPrimary: true }` to make the new provider the primary source instead.
671
672### Using `Effect.provideService`
673
674For one-off overrides without layers:
675
676```ts
677import { Config, ConfigProvider, Effect } from "effect"
678
679const provider = ConfigProvider.fromUnknown({ HOST: "localhost" })
680
681const program = Effect.gen(function*() {
682 const host = yield* Config.String("HOST")
683 return host
684}).pipe(
685 Effect.provideService(ConfigProvider.ConfigProvider, provider)
686)
687```
688
689## Two Ways to Run a Config
690
6911. **Yield in `Effect.gen`** — automatically uses the current `ConfigProvider` from the service map:
692
693 ```ts
694 const program = Effect.gen(function*() {
695 const host = yield* Config.String("HOST")
696 })
697 ```
698
6992. **Call `.parse(provider)` directly** — useful for testing or when you have a specific provider:
700
701 ```ts
702 const host = Config.String("HOST")
703 const result = Effect.runSync(host.parse(provider))
704 ```
705
706 The method accepts only the provider. Use `Config.nested` or the path argument of `Config.schema` to scope lookups.
707
708## Error Handling
709
710Config operations fail with `ConfigError`, which wraps either:
711
712- **`SourceError`** — the provider could not read data (I/O failure, permission error). Has `message` and optional `cause` properties.
713- **`SchemaError`** — data was found but didn't match the schema (wrong type, out of range, missing key).
714
715Check `error.cause._tag` to distinguish:
716
717```ts
718import { Config, ConfigProvider, Effect } from "effect"
719
720const program = Config.Int("PORT").parse(
721 ConfigProvider.fromUnknown({ PORT: "not-a-number" })
722).pipe(
723 Effect.tapError((error) =>
724 Effect.sync(() => {
725 if (error.cause._tag === "SchemaError") {
726 console.log("Validation failed:", error.message)
727 } else {
728 console.log("Source error:", error.message)
729 }
730 })
731 )
732)
733```
734
735**Important**: `Config.withDefault` and `Config.option` recover only from semantic absence. They do not classify `SchemaIssue` values as “missing.” Validation and source errors still propagate unless explicitly recovered with `Config.orElse`.
736
737## Practical Example: Web Server Config
738
739```ts
740import { Config, ConfigProvider, Effect, Schema } from "effect"
741
742// Define your config shape
743const ServerConfig = Config.schema(
744 Schema.Struct({
745 host: Schema.String,
746 port: Schema.Int,
747 logLevel: Schema.Literals(["debug", "info", "warn", "error"])
748 }),
749 "server"
750)
751
752const DbConfig = Config.schema(
753 Schema.Struct({
754 url: Schema.String,
755 poolSize: Schema.Int
756 }),
757 "db"
758)
759
760const AppConfig = Config.all({
761 server: ServerConfig,
762 db: DbConfig,
763 debug: Config.Boolean("debug").pipe(Config.withDefault(false))
764})
765
766// In production, just yield it — reads from process.env
767const program = Effect.gen(function*() {
768 const config = yield* AppConfig
769 console.log(config)
770})
771
772// For testing, provide a specific provider
773const testProvider = ConfigProvider.fromUnknown({
774 server: { host: "localhost", port: 3000, logLevel: "debug" },
775 db: { url: "postgres://localhost/testdb", poolSize: 5 },
776 debug: true
777})
778
779Effect.runSync(
780 program.pipe(Effect.provide(ConfigProvider.layer(testProvider)))
781)
782```
783
784With environment variables, the same config reads:
785
786```
787server_host=localhost
788server_port=3000
789server_logLevel=debug
790db_url=postgres://localhost/mydb
791db_poolSize=10
792debug=true
793```