watchEnv()
Watch for environment variable changes and re-validate automatically.
watchEnv()
Creates a reactive environment object that re-validates when the source changes. Useful during development when .env files are edited, or in long-running processes that need to pick up new variable values without restarting.
Signature
function watchEnv<T extends SchemaDefinition>(
schema: T,
opts?: WatchEnvOptions,
): WatchEnvResult<T>
Parameters
schema
A schema definition object where keys are environment variable names and values are validators, same as defineEnv().
opts (optional)
interface WatchEnvOptions extends DefineEnvOptions {
pollInterval?: number
onChange?: (key: string, oldValue: unknown, newValue: unknown) => void
onError?: (errors: readonly ValidationError[]) => void
}
| Option | Type | Default | Description |
|---|---|---|---|
source | EnvSource | Record | Auto-detected | The environment source to read from |
prefix | string | undefined | Prefix added to each key when looking up in the source |
maskWith | string | "********" | Custom mask string for secret values |
pollInterval | number | 500 | Interval in milliseconds between source polls |
onChange | function | undefined | Called per changed key when re-validation succeeds |
onError | function | undefined | Called when re-validation fails (env keeps old values) |
Return Value
type WatchEnvResult<T extends SchemaDefinition> = EnvResult<T> & {
readonly unwatch(): void
}
Same as EnvResult<T> from defineEnv() — a read-only Proxy with .meta access — plus:
| Method | Description |
|---|---|
.unwatch() | Stop polling and clean up the interval. Safe to call multiple times. |
Behavior
Initial Validation
watchEnv() runs the same validation as defineEnv() immediately. If any variables are missing or invalid, it throws a CtroEnvError with all errors collected.
Polling
After initialization, watchEnv() polls the source every pollInterval milliseconds. It compares the raw string values from source.get(key) against cached values from the previous poll. Only when at least one raw value changes does it re-run validation, making the poll loop efficient.
Re-validation Success
When re-validation passes, the underlying values object is updated. The Proxy reflects the new values immediately — no need to re-assign or re-create the env object. The onChange callback fires per changed key with the old and new parsed values.
Re-validation Failure
When re-validation fails, the env object keeps the last valid state. The onError callback fires with the validation errors, but the values you read from the env object are unchanged. This prevents your application from running with invalid environment values.
Cleanup
Call .unwatch() when you no longer need to watch for changes. This clears the interval and stops all polling. After calling .unwatch(), the env object remains usable — it keeps its last valid values — but no longer polls for updates.
Examples
Basic usage
import { watchEnv, string, number } from "@ctroenv/core"
import { loadEnv } from "@ctroenv/node"
const env = watchEnv(
{
DATABASE_URL: string().url(),
PORT: number().port().default(3000),
},
{
source: loadEnv(),
pollInterval: 1000,
},
)
// Use env like normal
console.log(env.PORT)
// Clean up when shutting down
process.on("SIGINT", () => {
env.unwatch()
process.exit(0)
})
Logging changes
const env = watchEnv(schema, {
source: loadEnv(),
onChange(key, oldVal, newVal) {
console.log(`[env] ${key}: ${oldVal} -> ${newVal}`)
},
})
Handling validation errors gracefully
const env = watchEnv(schema, {
source: loadEnv(),
onError(errors) {
console.error("[env] Re-validation failed, keeping previous values:")
for (const err of errors) {
console.error(` ${err.key}: ${err.message}`)
}
},
})
With secret masking
const env = watchEnv(
{
JWT_SECRET: string().min(32).secret(),
PORT: number().port().default(3000),
},
{ source: loadEnv() },
)
env.JWT_SECRET // "********"
env.meta.get("JWT_SECRET") // actual value
env.PORT // number
Comparison with defineEnv()
| Feature | defineEnv() | watchEnv() |
|---|---|---|
| Validation | Once at creation | Initial + re-validates on source changes |
| Throws on first error | Yes | Yes (initial). onError for re-validation |
| Proxy masking | Yes | Yes |
| Secret masking | Yes | Yes |
.meta access | Yes | Yes |
.unwatch() | No | Yes |
| Runtime overhead | None | Poll loop (configurable interval) |
| Use case | Server start, build time | Development, long-running processes, HMR |
How is this guide?
Last updated on Jun 30, 2026