Config
A Config<T> describes how to read and validate a single typed value of type
T from a ConfigProvider. Effect ships convenience constructors for the common
primitives, combinators for grouping and nesting them, and operators for
defaults, transformation, and fallback. Because every Config is also an
Effect, you consume it by yielding it inside Effect.gen.
import { Config, Effect } from "effect"
const program = Effect.gen(function* () { // Read and validate three values from the current ConfigProvider const host = yield* Config.string("HOST") // any string const port = yield* Config.port("PORT") // integer in 1–65535 const debug = yield* Config.boolean("DEBUG").pipe( // DEBUG is optional — fall back to false when it is missing Config.withDefault(false) )
yield* Effect.log(`http://${host}:${port} (debug=${debug})`)})
// HOST=localhost PORT=8080 DEBUG=yes node app.jsEffect.runFork(program)Each constructor takes the name of the key to read. With the default
environment-variable provider, Config.string("HOST") reads the HOST env var.
If a value is missing or fails validation, the program fails with a typed
Config.ConfigError rather than throwing.
Primitive constructors
Section titled “Primitive constructors”Effect provides constructors for the most common value types. Each one decodes the raw string from the provider and validates it:
| Constructor | Reads |
| ---------------------------- | ------------------------------------------------------------- |
| Config.string(name) | a string |
| Config.nonEmptyString(name)| a string that must contain at least one character |
| Config.number(name) | a number (allows NaN, Infinity) |
| Config.finite(name) | a finite number (rejects NaN, Infinity) |
| Config.int(name) | an integer (rejects floats) |
| Config.port(name) | an integer in 1–65535 |
| Config.boolean(name) | a boolean (true/false, yes/no, on/off, 1/0, y/n) |
| Config.literal(value, name)| exactly the given literal value |
| Config.literals(arr, name) | one of the given literal values |
| Config.duration(name) | a Duration (e.g. "10 seconds") |
| Config.url(name) | a URL |
| Config.date(name) | a valid Date |
| Config.logLevel(name) | a LogLevel ("Info", "Debug", …) |
| Config.redacted(name) | a secret wrapped in Redacted — see Redacted Secrets |
import { Config, Effect } from "effect"
const program = Effect.gen(function* () { const timeout = yield* Config.duration("REQUEST_TIMEOUT") // "30 seconds" → Duration const level = yield* Config.logLevel("LOG_LEVEL") // "Debug" → LogLevel "Debug" const env = yield* Config.literals( ["development", "staging", "production"], "NODE_ENV" )
yield* Effect.log(`Running in ${env} with timeout ${timeout}`)})Providing defaults and making values optional
Section titled “Providing defaults and making values optional”Not every key is required. Config.withDefault supplies a fallback value, and
Config.option turns a missing value into Option.None.
import { Config, Effect, Option } from "effect"
const program = Effect.gen(function* () { // Use 8080 when PORT is absent const port = yield* Config.port("PORT").pipe(Config.withDefault(8080))
// Option<string>: Some when MAX_CONNECTIONS is set, None otherwise const maxConn = yield* Config.option(Config.int("MAX_CONNECTIONS"))
yield* Effect.log(`port=${port}, maxConn=${Option.getOrElse(maxConn, () => -1)}`)})Combining configs
Section titled “Combining configs”Config.all combines several configs into one. Pass a record to get a named
struct back, or an array to get a tuple:
import { Config, Effect } from "effect"
// Combine into a named struct — the parsed result mirrors this shapeconst ServerConfig = Config.all({ host: Config.string("HOST"), port: Config.port("PORT")})
const program = Effect.gen(function* () { const server = yield* ServerConfig // ^? { host: string; port: number } yield* Effect.log(`${server.host}:${server.port}`)})Transforming and validating
Section titled “Transforming and validating”Config.map post-processes a parsed value with a pure function. When the
transformation itself can fail, use Config.mapOrFail, which returns an
Effect and can produce a ConfigError.
import { Config } from "effect"
// A reusable Config for a custom domain typeclass HostPort { constructor( readonly host: string, readonly port: number ) {} get url() { return `${this.host}:${this.port}` }}
const hostPort = Config.all([Config.string("HOST"), Config.port("PORT")]).pipe( // Map the tuple into a domain object once both values are validated Config.map(([host, port]) => new HostPort(host, port)))For richer validation — multiple fields, refinements, branded types — reach for
a Schema rather than hand-rolling mapOrFail. See the next section.
Reading structured config with a Schema
Section titled “Reading structured config with a Schema”Config.schema builds a Config from any Schema.Codec. This is the most
powerful constructor — every primitive constructor above is just a shortcut for
it. Use it to read and validate an entire nested struct in one shot.
import { Config, Effect, Schema } from "effect"
// Describe the whole config tree with a Schemaconst AppConfig = Config.schema( Schema.Struct({ host: Schema.String, port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })), features: Schema.Struct({ newDashboard: Schema.Boolean }) }), "app" // root path segment: keys live under the "app" namespace)
const program = Effect.gen(function* () { const config = yield* AppConfig // ^? { host: string; port: number; features: { newDashboard: boolean } } yield* Effect.log(`${config.host}:${config.port}`)})Validation failures from the schema (wrong type, out of range, missing key) are
wrapped in a Config.ConfigError whose cause is a SchemaError, so the error
message points at the exact path that failed.
Nesting configs under a namespace
Section titled “Nesting configs under a namespace”Config.nested scopes a config under a prefix. This keeps related keys grouped
and lets you reuse a config fragment at different paths.
import { Config, Effect } from "effect"
// Reusable fragment with un-prefixed keysconst connection = Config.all({ host: Config.string("HOST"), port: Config.port("PORT")})
const program = Effect.gen(function* () { // Reads DATABASE_HOST / DATABASE_PORT const db = yield* connection.pipe(Config.nested("DATABASE")) // Reads REDIS_HOST / REDIS_PORT const redis = yield* connection.pipe(Config.nested("REDIS"))
yield* Effect.log(`db=${db.host}:${db.port} redis=${redis.host}:${redis.port}`)})With the environment-variable provider, each nested segment becomes a
_-separated prefix on the env var name. With a JSON provider, it becomes an
extra object level. The mechanics of how a provider resolves these paths are
covered in Config Providers.
Handling config errors
Section titled “Handling config errors”A failing Config produces a Config.ConfigError. Its cause is either a
SourceError (the provider could not read the data) or a SchemaError
(validation failed). You can recover with the usual Effect error operators, or
fall back to another config with Config.orElse.
import { Config, Effect } from "effect"
const program = Effect.gen(function* () { // Try PRIMARY_URL first; if it errors for any reason, use the fallback const url = yield* Config.string("PRIMARY_URL").pipe( Config.orElse(() => Config.string("FALLBACK_URL")) ) yield* Effect.log(url)})