# How to Create Custom Refinements in Zod Using refine and superRefine

> Master Zod custom refinements with refine and superRefine. Learn to build robust validation, handle multiple errors, and control validation flow for your schemas.

- Repository: [Colin McDonnell/zod](https://github.com/colinhacks/zod)
- Tags: how-to-guide
- Published: 2026-02-23

---

**Use `refine` for simple true/false checks that return a single error message, and `superRefine` when you need to report multiple validation issues, customize error paths, or control whether validation continues after a failure.**

Zod is a TypeScript-first schema validation library that allows you to extend built-in validations with custom logic. In the `colinhacks/zod` repository, the `refine` and `superRefine` methods provide two distinct approaches for creating **custom refinements** that execute after standard type checks pass. Understanding the architectural differences between these APIs helps you choose the right tool for simple boolean validations versus complex multi-issue reporting.

## refine vs superRefine: Key Differences

Both methods attach custom validation logic to a schema, but they differ in flexibility and control:

- **`refine`** creates a single check that returns a boolean (or Promise<boolean>). When the check fails, Zod generates one issue with a custom message or the default `"custom"` error code. This is ideal for "pass/fail" validations.

- **`superRefine`** provides a **refinement context** (`ctx`) that allows you to push multiple issues, customize error paths, and decide whether validation should continue after a failure. Use this when you need granular error reporting or complex validation logic that produces several distinct error messages.

## Core Implementation Architecture

The heavy lifting for both methods happens in [`packages/zod/src/v4/core/api.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/api.ts), while public wrappers exist for both the Mini and Classic API flavors.

### Internal Core Functions

The `_refine` function (lines 16-24) creates a `ZodCheck` that runs a user-provided predicate:

```typescript
export function _refine<O = unknown, I = O>(
  Class: util.SchemaClass<schemas.$ZodCustom>,
  fn: (data: O) => unknown,
  _params: string | $ZodCustomParams | undefined
): schemas.$ZodCustom<O, I> {
  const schema = new Class({
    type: "custom",
    check: "custom",
    fn: fn as any,
    ...util.normalizeParams(_params),
  });
  return schema as any;
}

```

The `_superRefine` function (lines 36-56) builds a check that supplies a refinement context:

```typescript
export function _superRefine<T>(fn: (arg: T, payload: $RefinementCtx<T>) => void | Promise<void>) {
  const ch = _check<T>((payload) => {
    (payload as $RefinementCtx).addIssue = (issue) => {
      if (typeof issue === "string") {
        payload.issues.push(util.issue(issue, payload.value, ch._zod.def));
      } else {
        payload.issues.push(util.issue(_issue));
      }
    };
    return fn(payload.value, payload as $RefinementCtx<T>);
  });
  return ch;
}

```

### Public API Wrappers

Both Mini and Classic builds expose `refine` and `superRefine` as chainable methods:

- **Mini version** ([`packages/zod/src/v4/mini/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/mini/schemas.ts), lines 61-75):
  ```typescript
  export function refine<T>(fn, _params = {}) {
    return core._refine(ZodMiniCustom, fn, _params);
  }
  export function superRefine<T>(fn) {
    return core._superRefine(fn);
  }
  ```

- **Classic version** ([`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts), lines 15-27):
  ```typescript
  export function refine<T>(fn, _params = {}) {
    return core._refine(ZodCustom, fn, _params);
  }
  export function superRefine<T>(fn) {
    return core._superRefine(fn);
  }
  ```

## Practical Code Examples

### Simple Synchronous refine

Use `refine` for boolean checks that need a single error message:

```typescript
import { z } from "zod";

const passwordSchema = z
  .string()
  .min(8)
  .refine((val) => /[A-Z]/.test(val), { 
    message: "Must contain an uppercase letter" 
  });

passwordSchema.parse("short");         // ❌ throws "String must contain at least 8 characters"
passwordSchema.parse("lowercase123");  // ❌ throws "Must contain an uppercase letter"
passwordSchema.parse("ValidPass123");  // ✅ passes

```

### Asynchronous refine

`refine` automatically awaits promises returned by the predicate:

```typescript
import { z } from "zod";

const usernameSchema = z
  .string()
  .refine(
    async (val) => {
      const taken = await db.users.findUnique({ where: { username: val } });
      return !taken;
    },
    { message: "Username already taken" }
  );

await usernameSchema.parseAsync("alice"); // ✅ if not taken
await usernameSchema.parseAsync("bob");   // ❌ "Username already taken"

```

### Custom Error Paths with refine

Target specific fields in object schemas:

```typescript
import { z } from "zod";

const pointSchema = z.object({
  x: z.number(),
  y: z.number(),
}).refine(
  (pt) => pt.x >= 0 && pt.y >= 0,
  {
    message: "Coordinates must be non‑negative",
    path: ["x", "y"],
  }
);

```

### Multiple Issues with superRefine

Use `superRefine` when you need to report several distinct errors:

```typescript
import { z } from "zod";

const registrationSchema = z.object({
  password: z.string().min(8),
  confirm: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirm) {
    ctx.addIssue({
      code: "custom",
      message: "Passwords do not match",
      path: ["confirm"],
    });
  }
  if (/\s/.test(data.password)) {
    ctx.addIssue({
      code: "custom",
      message: "Password cannot contain spaces",
      continue: false, // abort further validation
    });
  }
});

```

### Chaining Both Methods

Combine `refine` and `superRefine` for layered validation:

```typescript
import { z } from "zod";

const userSchema = z.object({
  email: z.string().email(),
  age: z.number(),
})
  .refine((obj) => obj.age >= 18, { message: "Must be adult" })
  .superRefine((obj, ctx) => {
    if (obj.email.endsWith("@example.com")) {
      ctx.addIssue({
        code: "custom",
        message: "Corporate emails not allowed",
        path: ["email"],
      });
    }
  });

```

## Abort and Continue Behavior

Understanding when validation stops is crucial for error collection:

- **`refine`**: By default, `abort: true` stops validation after the first failing refinement. You can override this by passing `{ abort: false }` in the params.

- **`superRefine`**: Each issue added via `ctx.addIssue` can set `continue: true` to keep validating, or `continue: false` to abort. The default behavior is `continue = !schema._zod.def.abort`, meaning it respects the schema's abort setting unless explicitly overridden.

This logic is implemented in [`packages/zod/src/v4/core/api.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/api.ts) around lines 49-50 and in the utility functions within [`packages/zod/src/v4/core/util.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/util.ts).

## Summary

- **`refine`** is the lightweight choice for single boolean checks that need one error message; it wraps your predicate in a `ZodCheck` via `_refine` in [`packages/zod/src/v4/core/api.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/api.ts).
- **`superRefine`** provides full control via a refinement context (`ctx`) that supports `addIssue`, custom paths, and continue/abort logic; implemented in `_superRefine` in the same core file.
- Both methods are exposed through public wrappers in [`packages/zod/src/v4/mini/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/mini/schemas.ts) (Mini API) and [`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts) (Classic API).
- Use `refine` for simple validations like password strength or uniqueness checks, and `superRefine` when you need to report multiple distinct errors or manipulate error paths dynamically.

## Frequently Asked Questions

### What is the difference between refine and superRefine in Zod?

`refine` is designed for simple validations that return a boolean or Promise<boolean>, producing a single error message when the check fails. `superRefine` provides a context object (`ctx`) that allows you to add multiple issues with custom paths, error codes, and control over whether validation continues after each error. Choose `refine` for "pass/fail" logic and `superRefine` when you need granular error reporting.

### Can I use async functions with Zod refinements?

Yes, both `refine` and `superRefine` support asynchronous predicates. When you return a Promise from your validation function, Zod automatically awaits it during parsing. Use `parseAsync` or `safeParseAsync` on your schema to handle the asynchronous validation properly, particularly when checking database uniqueness or external API availability.

### How do I add multiple error messages to a single field?

Use `superRefine` with the context's `addIssue` method to push multiple validation errors for the same or different fields. Unlike `refine`, which stops at the first failure (by default), `superRefine` lets you collect all validation issues by setting `continue: true` on each issue, giving users a complete list of what needs correction rather than one error at a time.

### Where are the refine and superRefine methods implemented in the Zod source code?

The core logic resides in [`packages/zod/src/v4/core/api.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/api.ts), where the internal `_refine` and `_superRefine` functions create `ZodCheck` instances. The public-facing methods you import from `zod` are thin wrappers located in [`packages/zod/src/v4/mini/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/mini/schemas.ts) (for the Mini build) and [`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts) (for the Classic build), which delegate to the core implementations while attaching the appropriate schema class.