SafeFn
Creating a SafeFn

Handler functions

SafeFn has two types of handler functions, handler() and safeHandler().

Args

Both handler() and safeHandler() take in an object with the following properties:

  • input: The result of parsing the input schema if you defined one, otherwise undefined.
  • unsafeRawInput: The unparsed input of your function. Either inferred through input() or set through unparsedInput(), and merged with the unsafeRawInput of the parent SafeFn if one is defined. undefined if there's no input schema, no unparsedInput() set, and no parent is set or a parent without input is used.
  • ctx: The Ok return value of the parent handler function or undefined if no parent handler function is set.
  • ctxInput: An array of all the parsed input of the parent handler functions. This could be handy in a pinch, altho ideally you'd return the data from the parent function directly and access it through ctx.

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!

Handler

handler() takes in a function that will be called when your SafeFn is executed. This function should return a Result which can be created through Neverthrow's ok() and err() functions. If you defined an output schema, the Ok return must match the shape of the input of that schema.

const mySafeFunction = createSafeFn()
  .unparsedInput<{ firstName: string; lastName: string }>()
  .output(z.object({ fullName: z.string() }))
  .handler((args) => {
    // Error: Type { name: string; } is not assignable to type { fullName: string; }
    return ok({
      name: `${args.unsafeRawInput.firstName} ${args.unsafeRawInput.lastName}`,
    });
  });

Safe Handler

Instead of using handler(), you can also use safeHandler(). This offers the same functionality as NeverThrow's safeTry() and takes in an async generator function. This is probably the route you want to take if the rest of your codebase is built with NeverThrow, as it allows ergonomic "return if error" handling.

Consider the following example:

// Fake function declarations
declare function getUserTodoList(): ResultAsync<string[], { code: "DB_ERROR" }>;
declare function getTodoById(
  id: string,
): ResultAsync<
  { id: string; title: string; description: string },
  { code: "DB_ERROR" }
>;
 
const getLastUserTodoTitle = createSafeFn().handler(async () => {
  const todoList = await getUserTodoList();
 
  if (todoList.isErr()) {
    return todoList;
  }
  if (!todoList.value.length) {
    return err({
      code: "NO_TODOS",
    });
  }
  const lastTodoId = todoList.value[todoList.value.length - 1];
 
  const todo = await getTodoById(lastTodoId);
 
  if (todo.isErr()) {
    return todo;
  }
  return ok(todo.value.title);
});

This can be rewritten using safeHandler() as follows:

const getLastUserTodoTitle = createSafeFn().handler(async function* () {
  // Read as: run getUserTodolist() and assign the result value to todoList if it's okay,
  // otherwise short-circuit the entire function and return error.
  const todoList = yield* getUserTodoList().safeUnwrap();
  if (!todoList.length) {
    return err({
      code: "NO_TODOS",
    });
  }
  const lastTodoId = todoList[todoList.length - 1];
  const todo = yield* getTodoById(lastTodoId).safeUnwrap();
  return ok(todo.title);
});

On this page