SafeFn
Creating a SafeFn

Input and Output

Input schema

You can set an input schema for your SafeFn by using input(). This takes in a Zod schema and will type the input and unsafeRawInput arguments for your handler and callback functions as well as the required input to execute the SafeFn.

const mySafeFunction = SafeFn.new()
  .input(z.object({ firstName: z.string(), lastName: z.string() }))
  .handler((args) => {
    /*        ^ ?
      { input: { firstName: string; lastName: string };
        unsafeRawInput: { firstName: string; lastName: string }};
        ...
     */
    return ok({
      fullName: `${args.input.firstName} ${args.input.lastName}`,
    });
  });

When parsing is not successful, your handler function is not called and the error is returned. As such, the following types are added to the return type of the function:

  • when using run()
ResultAsync<{
  never,
  {
    code: "INPUT_PARSING";
    cause: z.ZodError<{
      firstName: string;
    lastName: string;
  }>;
  }
}>;
  • when using createAction()() (full errors are stripped here as classes can not be sent via a Server Action and stack traces should not be shipped to the client)
ActionResult<
  never,
  {
    code: "INPUT_PARSING";
    cause: {
      formattedError: z.ZodFormattedError<{
        firstName: string;
        lastName: string;
      }>;
      flattenedError: z.typeToFlattenedError<
        {
          firstName: string;
          lastName: string;
        },
        string
      >;
    };
  }
>;

When using a parent, the schema of the Zod error is merged with the possible input parsing errors from all parent SafeFns. This is done as it's hard to discriminate on the Zod errors otherwise, and all schema properties are optional in it anyway.

Other than the expected parse errors, Zod can also throw unexpectedly when using features like coerce or passing functions to transform(). These will be caught and passed to your uncaught error handler using catch().

Unparsed input

If you don't want to parse the input, but still want to type it you can use unparsedInput<T>(). This function returns the same instance, but allows you to easily set the unsafeRawInput through a generic argument. This types the unsafeRawInput argument of your handler and callback functions, as well as the input that's needed to run your SafeFn.

While this adds type safety, it's highly recommended to use an input schema if your function will be exposed as a Server Action or regular API endpoint as types aren't checked at runtime.

When chaining the type for unsafeRawInput can lie if you use this SafeFn as a parent for another! Since the input is not parsed and the child SafeFn might require additional input that will be passed in at runtime, the parent function will be called with properties that only appear in the type of the child function!

const mySafeFunction = SafeFn.new()
  .unparsedInput<{ firstName: string; lastName: string }>()
  .output(z.object({ fullName: z.string() }))
  .handler((args) => {
    /*        ^ ?
      { input: undefined;
        unsafeRawInput: { firstName: string; lastName: string }};
        ...
     */
    return ok({
      fullName: `${args.unsafeRawInput.firstName} ${args.unsafeRawInput.lastName}`,
    });
  });

Output schema

You can set an output schema for your SafeFn by using output(). This takes in a Zod schema and will type the required return type of your handler function, as well as the return type when running your SafeFn.

const mySafeFunction = SafeFn.new()
  .input(z.object({ firstName: z.string(), lastName: z.string() }))
  .output(z.object({ fullName: z.string() }))
  .handler((args) => {
    return ok({
      fullName: `${args.input.firstName} ${args.input.lastName}`,
    });
  });

When your handler returns an Err, output parsing is skipped. If your handler can only return an Err result, the possible error types from the output schema are omitted from the return type.

When output parsing is not successful, an Err is returned. The following types are added to the return type:

  • when using run()
ResultAsync<{
  never,
  {
    code: "OUTPUT_PARSING";
    cause: z.ZodError<{ fullName: string }>;
  };
}>;
  • when using createAction()() (full errors are stripped here as classes can not be sent via a Server Action and stack traces should not be shipped to the client)
ActionResult<
  never,
  {
    code: "OUTPUT_PARSING";
    cause: {
      formattedError: z.ZodFormattedError<{ fullName: string }>;
      flattenedError: z.typeToFlattenedError<{ fullName: string }, string>;
    };
  }
>;

When using a parent, the schema of the Zod error is merged with the possible output parsing errors from all parent SafeFns. This is done as it's hard to discriminate on the Zod errors otherwise, and all schema properties are optional in it anyway.

Other than the expected parse errors, Zod can also throw unexpectedly when using features like coerce or passing functions to transform(). These will be caught and passed to your uncaught error handler using catch().

On this page