CtroEnv
ctroenvType-Safe Environment Variables
Getting StartedQuick StartCore Concepts
defineEnv()string()number()boolean()semver()pick()ip(), ipv4(), ipv6()uuid(), guid()Chainable MethodsRefinementsError HandlingSchema CompositionSecurityCustom ValidatorswatchEnv()
CLI Overviewctroenv validatectroenv generatectroenv checkctroenv docsctroenv initCLI Configuration
Node AdapterVite AdapterNext.js Adapter
Migration from t3-envMigration from envalidMigration from dotenv

watchEnv()

Watch for environment variable changes and re-validate automatically.

  1. Docs
  2. Core API

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
}
OptionTypeDefaultDescription
sourceEnvSource | RecordAuto-detectedThe environment source to read from
prefixstringundefinedPrefix added to each key when looking up in the source
maskWithstring"********"Custom mask string for secret values
pollIntervalnumber500Interval in milliseconds between source polls
onChangefunctionundefinedCalled per changed key when re-validation succeeds
onErrorfunctionundefinedCalled 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:

MethodDescription
.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()

FeaturedefineEnv()watchEnv()
ValidationOnce at creationInitial + re-validates on source changes
Throws on first errorYesYes (initial). onError for re-validation
Proxy maskingYesYes
Secret maskingYesYes
.meta accessYesYes
.unwatch()NoYes
Runtime overheadNonePoll loop (configurable interval)
Use caseServer start, build timeDevelopment, long-running processes, HMR

How is this guide?

Edit on GitHub

Last updated on Jun 30, 2026

PreviousCustom ValidatorsNextCLI Overview

On this page

SignatureParametersschemaopts (optional)Return ValueBehaviorInitial ValidationPollingRe-validation SuccessRe-validation FailureCleanupExamplesBasic usageLogging changesHandling validation errors gracefullyWith secret maskingComparison with defineEnv()