Skip to content

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.js
Effect.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.

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 165535 | | 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)}`)
})

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 shape
const 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}`)
})

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 type
class 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.

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 Schema
const 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.

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 keys
const 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.

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)
})