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:

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

Every constructor below takes an optional name — the root path segment to read from the provider. Omit name when the config is a fragment of a larger schema (it will read at the current path). Each primitive is a thin shortcut over Config.schema with a specific codec.

ConstructorCodecResult
Config.string(name?)Schema.Stringstring
Config.nonEmptyString(name?)Schema.NonEmptyStringstring
Config.number(name?)Schema.Numbernumber
Config.finite(name?)Schema.Finitenumber
Config.int(name?)Schema.Intnumber
Config.port(name?)Config.Portnumber
Config.boolean(name?)Config.Booleanboolean
Config.literal(value, name?)Schema.Literal(value)the literal
Config.literals(arr, name?)Schema.Literals(arr)one of the set
Config.duration(name?)Schema.DurationFromStringDuration
Config.url(name?)Schema.URLURL
Config.date(name?)Schema.DateValidDate
Config.logLevel(name?)Config.LogLevelLogLevel
Config.redacted(name?)Schema.Redacted(Schema.String)Redacted<string>
Config.schema(codec, path?)any Schema.Codecthe codec’s type
Config.succeed(value)constant
Config.fail(err)always errors
Config.make(parse)custom

Reads a string using Schema.String. Any string value is accepted.

import { Config, ConfigProvider, Effect } from "effect"
const host = Config.string("HOST")
const provider = ConfigProvider.fromUnknown({ HOST: "localhost" })
Effect.runSync(host.parse(provider))
// => "localhost"

Reads a string but rejects the empty string (Schema.NonEmptyString).

import { Config, ConfigProvider, Effect } from "effect"
const name = Config.nonEmptyString("APP_NAME")
Effect.runSync(name.parse(ConfigProvider.fromUnknown({ APP_NAME: "api" })))
// => "api"
Effect.runSync(name.parse(ConfigProvider.fromUnknown({ APP_NAME: "" })))
// => throws ConfigError (empty string rejected)

Reads a number using Schema.Number. Allows NaN and Infinity.

import { Config, ConfigProvider, Effect } from "effect"
const ratio = Config.number("RATIO")
Effect.runSync(ratio.parse(ConfigProvider.fromUnknown({ RATIO: "1.5" })))
// => 1.5

Reads a finite number using Schema.Finite — rejects NaN and Infinity.

import { Config, ConfigProvider, Effect } from "effect"
const factor = Config.finite("FACTOR")
Effect.runSync(factor.parse(ConfigProvider.fromUnknown({ FACTOR: "2.0" })))
// => 2
Effect.runSync(factor.parse(ConfigProvider.fromUnknown({ FACTOR: "Infinity" })))
// => throws ConfigError (not finite)

Reads an integer using Schema.Int — rejects floats.

import { Config, ConfigProvider, Effect } from "effect"
const retries = Config.int("MAX_RETRIES")
Effect.runSync(retries.parse(ConfigProvider.fromUnknown({ MAX_RETRIES: "3" })))
// => 3
Effect.runSync(retries.parse(ConfigProvider.fromUnknown({ MAX_RETRIES: "3.5" })))
// => throws ConfigError (not an integer)

Reads an integer in 165535 (the Config.Port schema).

import { Config, ConfigProvider, Effect } from "effect"
const port = Config.port("PORT")
Effect.runSync(port.parse(ConfigProvider.fromUnknown({ PORT: "8080" })))
// => 8080
Effect.runSync(port.parse(ConfigProvider.fromUnknown({ PORT: "70000" })))
// => throws ConfigError (out of range)

Reads a boolean using the Config.Boolean schema. Accepted truthy strings: true, yes, on, 1, y; falsy: false, no, off, 0, n.

import { Config, ConfigProvider, Effect } from "effect"
const debug = Config.boolean("DEBUG")
Effect.runSync(debug.parse(ConfigProvider.fromUnknown({ DEBUG: "yes" })))
// => true
Effect.runSync(debug.parse(ConfigProvider.fromUnknown({ DEBUG: "0" })))
// => false

Accepts exactly one literal value (Schema.Literal). The literal can be a string, number, boolean, null, or bigint (AST.LiteralValue).

import { Config, ConfigProvider, Effect } from "effect"
const mode = Config.literal("production", "MODE")
Effect.runSync(mode.parse(ConfigProvider.fromUnknown({ MODE: "production" })))
// => "production"
Effect.runSync(mode.parse(ConfigProvider.fromUnknown({ MODE: "dev" })))
// => throws ConfigError

Accepts one of a fixed set of literal values (Schema.Literals).

import { Config, ConfigProvider, Effect } from "effect"
const env = Config.literals(["development", "production"], "NODE_ENV")
Effect.runSync(env.parse(ConfigProvider.fromUnknown({ NODE_ENV: "production" })))
// => "production"

Reads a Duration from a human-readable string (Schema.DurationFromString), e.g. "10 seconds", "500 millis", "Infinity".

import { Config, ConfigProvider, Effect } from "effect"
const timeout = Config.duration("TIMEOUT")
Effect.runSync(timeout.parse(ConfigProvider.fromUnknown({ TIMEOUT: "10 seconds" })))
// => Duration { _tag: "Millis", millis: 10000 }

Reads a URL, parsing the string with the URL constructor (Schema.URL). Fails if the string is not a valid URL.

import { Config, ConfigProvider, Effect } from "effect"
const apiUrl = Config.url("API_URL")
const url = Effect.runSync(
apiUrl.parse(ConfigProvider.fromUnknown({ API_URL: "https://example.com" }))
)
url.href
// => "https://example.com/"

Reads a valid Date (Schema.DateValid). Fails with a SchemaError if the string produces an invalid Date.

import { Config, ConfigProvider, Effect } from "effect"
const createdAt = Config.date("CREATED_AT")
Effect.runSync(createdAt.parse(ConfigProvider.fromUnknown({ CREATED_AT: "2024-01-15" })))
// => Date("2024-01-15T00:00:00.000Z")

Reads an Effect LogLevel (the Config.LogLevel schema). Accepted values: All, Fatal, Error, Warn, Info, Debug, Trace, None.

import { Config, ConfigProvider, Effect } from "effect"
const level = Config.logLevel("LOG_LEVEL")
Effect.runSync(level.parse(ConfigProvider.fromUnknown({ LOG_LEVEL: "Info" })))
// => "Info"

Reads a secret string wrapped in a Redacted container that hides the value from logs and toString (Schema.Redacted(Schema.String)). See Redacted Secrets for working with the result.

import { Config, ConfigProvider, Effect } from "effect"
const apiKey = Config.redacted("API_KEY")
const secret = Effect.runSync(
apiKey.parse(ConfigProvider.fromUnknown({ API_KEY: "sk-123" }))
)
console.log(secret)
// => <redacted>

The most general constructor: builds a Config<T> from any Schema.Codec. Every primitive constructor above delegates to it. The optional path sets the root segment(s) — pass a string for a flat key or an array for a nested path.

import { Config, ConfigProvider, Effect, Schema } from "effect"
// A struct read under the "db" namespace
const DbConfig = Config.schema(
Schema.Struct({ host: Schema.String, port: Schema.Int }),
"db"
)
Effect.runSync(
DbConfig.parse(
ConfigProvider.fromUnknown({ db: { host: "localhost", port: 5432 } })
)
)
// => { host: "localhost", port: 5432 }
// A custom nested path with an array
const deep = Config.schema(Schema.String, ["a", "b", "c"])
Effect.runSync(
deep.parse(ConfigProvider.fromUnknown({ a: { b: { c: "hi" } } }))
)
// => "hi"

Creates a config that ignores the provider and always succeeds with value. Useful as a constant fallback inside Config.orElse.

import { Config, ConfigProvider, Effect } from "effect"
const host = Config.string("HOST").pipe(
Config.orElse(() => Config.succeed("localhost"))
)
Effect.runSync(host.parse(ConfigProvider.fromUnknown({})))
// => "localhost"

Creates a config that always fails with a ConfigError wrapping the given SourceError or SchemaError. Used inside orElse to re-raise a specific error.

import { Config } from "effect"
// Inside an orElse handler, re-raise the original cause instead of recovering
const reRaise = (cause: Parameters<typeof Config.fail>[0]) => Config.fail(cause)

Low-level constructor: builds a Config from a function that takes a ConfigProvider and returns Effect<T, ConfigError>. Every primitive and combinator is built on it; reach for it only when schema and the combinators cannot express what you need.

import { Config, ConfigProvider, Effect } from "effect"
const hostPort = Config.make((provider) =>
Effect.all({
host: Config.string("host").parse(provider),
port: Config.number("port").parse(provider)
})
)
Effect.runSync(hostPort.parse(ConfigProvider.fromUnknown({ host: "localhost", port: 3000 })))
// => { host: "localhost", port: 3000 }

Post-processes the parsed value with a pure function that cannot fail. Available data-first and data-last (pipeable).

import { Config, ConfigProvider, Effect } from "effect"
const upper = Config.string("NAME").pipe(Config.map((s) => s.toUpperCase()))
Effect.runSync(upper.parse(ConfigProvider.fromUnknown({ NAME: "alice" })))
// => "ALICE"

Like map, but f returns Effect<B, ConfigError> so the transformation can fail with a config error.

import { Config, ConfigProvider, Effect } from "effect"
const trimmed = Config.string("NAME").pipe(
Config.mapOrFail((s) => Effect.succeed(s.trim()))
)
Effect.runSync(trimmed.parse(ConfigProvider.fromUnknown({ NAME: " bob " })))
// => "bob"

Catches all ConfigErrors (including validation errors) and switches to the fallback config. The that callback receives the error.

import { Config, ConfigProvider, Effect } from "effect"
const url = Config.string("PRIMARY_URL").pipe(
Config.orElse(() => Config.string("FALLBACK_URL"))
)
Effect.runSync(url.parse(ConfigProvider.fromUnknown({ FALLBACK_URL: "https://b" })))
// => "https://b"

Supplies a fallback value, but only when the failure is caused by missing data (a missing key or undefined). Validation errors still propagate.

import { Config, ConfigProvider, Effect } from "effect"
const port = Config.number("PORT").pipe(Config.withDefault(3000))
Effect.runSync(port.parse(ConfigProvider.fromUnknown({})))
// => 3000
Effect.runSync(port.parse(ConfigProvider.fromUnknown({ PORT: "not-a-number" })))
// => throws ConfigError (validation error is NOT defaulted)

Returns Some(value) on success and None when the data is missing. Like withDefault, only missing-data errors produce None; validation errors still propagate.

import { Config, ConfigProvider, Effect } from "effect"
const maybePort = Config.option(Config.number("PORT"))
Effect.runSync(maybePort.parse(ConfigProvider.fromUnknown({})))
// => { _tag: "None" }
Effect.runSync(maybePort.parse(ConfigProvider.fromUnknown({ PORT: "8080" })))
// => { _tag: "Some", value: 8080 }

Combines multiple configs, mirroring the input shape: a record yields a struct, an array (or any iterable) yields a tuple/array.

import { Config, ConfigProvider, Effect } from "effect"
// Record → struct
const asStruct = Config.all({
host: Config.string("host"),
port: Config.number("port")
})
Effect.runSync(asStruct.parse(ConfigProvider.fromUnknown({ host: "localhost", port: 5432 })))
// => { host: "localhost", port: 5432 }
// Array → tuple
const asTuple = Config.all([Config.string("host"), Config.number("port")])
Effect.runSync(asTuple.parse(ConfigProvider.fromUnknown({ host: "localhost", port: 5432 })))
// => ["localhost", 5432]

Prefixes every key the inner config reads with name. With fromEnv this is a _-joined prefix; with fromUnknown/JSON it is an extra object level. Multiple nested calls compose, outermost first.

import { Config, ConfigProvider, Effect } from "effect"
const host = Config.string("host").pipe(Config.nested("database"))
// JSON: extra object level
Effect.runSync(host.parse(ConfigProvider.fromUnknown({ database: { host: "localhost" } })))
// => "localhost"
// Env: "_"-joined prefix (reads database_host)
Effect.runSync(host.parse(ConfigProvider.fromEnv({ env: { database_host: "localhost" } })))
// => "localhost"

These exported schemas back the primitive constructors. Use them directly with Config.schema — for example to read a value at a custom path, or to embed inside a larger Schema.Struct.

Schema decoding a string into a boolean. Accepted strings: true, false, yes, no, on, off, 1, 0, y, n.

import { Config, ConfigProvider, Effect } from "effect"
const flag = Config.schema(Config.Boolean, "ENABLED")
Effect.runSync(flag.parse(ConfigProvider.fromUnknown({ ENABLED: "on" })))
// => true

Schema for an integer in 165535 (Schema.Int with a between check).

import { Config, ConfigProvider, Effect } from "effect"
const port = Config.schema(Config.Port, "PORT")
Effect.runSync(port.parse(ConfigProvider.fromUnknown({ PORT: "443" })))
// => 443

Schema of LogLevel string literals: All, Fatal, Error, Warn, Info, Debug, Trace, None.

import { Config, ConfigProvider, Effect } from "effect"
const level = Config.schema(Config.LogLevel, "LOG_LEVEL")
Effect.runSync(level.parse(ConfigProvider.fromUnknown({ LOG_LEVEL: "Debug" })))
// => "Debug"

Builds a Schema (a union of a record and a flat-string parser) for a key-value map. It accepts either a structured record from the provider or a flat string like "k=v,k2=v2". Customize with separator (default ",") and keyValueSeparator (default "="). Use it via Config.schema.

import { Config, ConfigProvider, Effect, Schema } from "effect"
const attributes = Config.schema(
Config.Record(Schema.String, Schema.String),
"OTEL_RESOURCE_ATTRIBUTES"
)
const provider = ConfigProvider.fromEnv({
env: {
OTEL_RESOURCE_ATTRIBUTES:
"service.name=my-service,service.version=1.0.0,custom.attribute=value"
}
})
Effect.runSync(attributes.parse(provider))
// => {
// "service.name": "my-service",
// "service.version": "1.0.0",
// "custom.attribute": "value"
// }

The literal schemas backing Config.Boolean: Config.TrueValues is ["true", "yes", "on", "1", "y"] and Config.FalseValues is ["false", "no", "off", "0", "n"]. These are internal-ish but exported for reuse.

import { Config } from "effect"
Config.TrueValues.literals
// => ["true", "yes", "on", "1", "y"]
Config.FalseValues.literals
// => ["false", "no", "off", "0", "n"]

Config.Wrap<A> recursively replaces the primitives in a structure with Configs, so a caller can pass either a single Config<A> or a record of individual Configs. Config.unwrap collapses such a value back into a single Config<A>.

import { Config, ConfigProvider, Effect } from "effect"
interface Options {
host: string
port: number
}
// Accept either Config<Options> or { host: Config<string>; port: Config<number> }
const makeConfig = (config: Config.Wrap<Options>): Config.Config<Options> =>
Config.unwrap(config)
const config = makeConfig({
host: Config.string("host"),
port: Config.number("port")
})
Effect.runSync(config.parse(ConfigProvider.fromUnknown({ host: "localhost", port: 5432 })))
// => { host: "localhost", port: 5432 }

Config.Success<T> extracts the parsed value type from a Config:

import type { Config } from "effect"
type Host = Config.Success<Config.Config<string>>
// => string

Type guard returning true when u is a Config. Used internally by unwrap and when narrowing an unknown value before calling .parse().

import { Config } from "effect"
Config.isConfig(Config.string("HOST"))
// => true
Config.isConfig("not a config")
// => false

The error class produced by a failing config. Its cause is either a SourceError (the provider could not read data) or a Schema.SchemaError (validation failed). Match on error.cause._tag to distinguish the two.

import { Config, ConfigProvider, Effect, Schema } from "effect"
const program = Config.int("PORT").pipe(
Effect.catch((error: Config.ConfigError) =>
Effect.succeed(
Schema.isSchemaError(error.cause) ? "validation failed" : "could not read"
)
)
)
Effect.runSync(program.pipe(
Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({ PORT: "x" }))
))
// => "validation failed"

The core interface. A Config<T> extends Effect<T, ConfigError>, so it can be yielded directly in Effect.gen (resolving the current ConfigProvider from context) or run against a specific provider with .parse(provider). It is also pipeable, so config.pipe(Config.map(...)) works as shown throughout this page.