> ## Documentation Index
> Fetch the complete documentation index at: https://next-safe-env.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Validation errors and custom error handlers

> next-safe-env collects all validation failures before exiting, printing a complete error list. Customize behavior with onValidationError for structured logging.

`next-safe-env` follows one rule for validation failures: fail at startup, never at runtime. When `createEnv()` is called at module load time, every field in the schema is validated before the function returns. If any field fails, all failures are collected first - the engine never short-circuits on the first error - and the process exits with a non-zero code after printing a complete list of every problem at once.

## What the error output looks like

When validation fails, you see a structured message that lists every bad field along with what was expected, what was actually received, and a summary of how many server and client vars passed or failed:

```
[next-safe-env] Environment validation failed - 3 error(s):

  ✗ DATABASE_URL     - Required. Expected a valid URL. Got: "postgres-localhost"
  ✗ JWT_SECRET       - Too short. Must be ≥ 32 characters. Got length: 12
  ✗ SMTP_PORT        - Invalid port. Must be 1–65535. Got: "99999"

  Server vars:  2 valid, 3 invalid
  Client vars:  2 valid, 0 invalid

  Set the correct values in your .env file or deployment environment and restart.
```

Every failure line includes:

* **Field name** - the exact key from your schema, left-padded for alignment
* **What was expected** - the constraint that was not satisfied (e.g. `valid URL`, `length >= 32`, `<= 65535`)
* **What was received** - the raw string value that was present, or `"undefined"` if the variable was missing

The summary block counts server and client vars independently, so you can see at a glance whether the failures are concentrated in one layer.

## The `EnvValidationError` class

When validation fails, `next-safe-env` creates an instance of `EnvValidationError` and either passes it to your `onValidationError` handler or, if none is provided, calls `console.error(err.format())` and `process.exit(1)` by default.

```typescript theme={null}
class EnvValidationError extends Error {
  readonly failures: ValidationFailure[]
  readonly stats: ValidationStats

  format(): string
  toJSON(): unknown
}
```

### `failures: ValidationFailure[]`

An array containing one entry per failed field. Each entry has the shape:

```typescript theme={null}
type ValidationFailure = {
  field:    string  // the schema key name
  expected: string  // human-readable description of the expected value
  received: string  // the raw value that was present
  message:  string  // full message: `Expected ${expected}. Got: "${received}"`
}
```

### `stats: ValidationStats`

An object with counts broken down by server and client:

```typescript theme={null}
type ValidationStats = {
  serverTotal:  number  // total server schema keys
  clientTotal:  number  // total client schema keys
  serverFailed: number  // server keys that failed validation
  clientFailed: number  // client keys that failed validation
}
```

### `format(): string`

Returns the full pretty-printed multi-line string shown in the output above. This is what the default handler passes to `console.error`. Call it when you want to log the human-readable version.

### `toJSON(): unknown`

Returns a plain JSON-serializable object:

```typescript theme={null}
{
  error:    'EnvValidationError',
  failures: ValidationFailure[],
  stats:    ValidationStats,
}
```

Use `toJSON()` when you need to send the error to a structured logging system, a remote error tracker, or any destination that expects JSON rather than a formatted string.

## Custom error handlers with `onValidationError`

You can replace the default `console.error` + `process.exit(1)` behavior by providing an `onValidationError` function in the config:

```typescript theme={null}
import { createEnv, url, str } from 'next-safe-env'

export const env = createEnv({
  server: {
    DATABASE_URL: url(),
    JWT_SECRET:   str().min(32),
  },
  client: {},
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    JWT_SECRET:   process.env.JWT_SECRET,
  },
  onValidationError: (err) => {
    myLogger.fatal({ env_errors: err.toJSON() }, 'Environment validation failed')
    process.exit(1)
  },
})
```

The handler receives the `EnvValidationError` instance. You have access to `err.failures`, `err.stats`, `err.format()`, and `err.toJSON()` - use whichever form suits your logging infrastructure.

<Warning>
  `onValidationError` must never return. The engine does not call `process.exit()` after invoking your handler. If the handler returns normally, execution continues with an invalid env object - every field that failed validation will be `undefined` or the wrong type, and your application will behave unpredictably. Always call `process.exit()` or throw an error inside the handler.
</Warning>

A handler for a structured logging setup might look like this:

```typescript theme={null}
import { createEnv, url, str, port } from 'next-safe-env'
import { logger } from './logger'

export const env = createEnv({
  server: {
    DATABASE_URL: url(),
    JWT_SECRET:   str().min(32),
    PORT:         port().default(3000),
  },
  client: {},
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    JWT_SECRET:   process.env.JWT_SECRET,
    PORT:         process.env.PORT,
  },
  onValidationError: (err) => {
    logger.fatal(
      {
        failureCount: err.failures.length,
        failures:     err.failures,
        stats:        err.stats,
      },
      '[next-safe-env] Startup aborted: invalid environment configuration',
    )
    process.exit(1)
  },
})
```

This sends the structured `failures` array and `stats` object to your log aggregator while preserving the non-zero exit code that signals a failed deployment to your process manager or container orchestrator.

<Note>
  The `onValidationError` option accepts a function typed as `(error: ValidationErrorShape) => never`. The `never` return type is enforced by TypeScript - if your handler has a code path that could return, the compiler will error.
</Note>
