Introduction
Why SafeFn?
SafeFn is a library to easily build fully typed functions with validated input and output, procedure chaining, callbacks and more. This is ideal for functions that are at the edge of your applications, like Server Actions!
Existing solutions like ZSA and Next-Safe-Action are great, but often fall short when it comes to properly handling errors. SafeFn integrates NeverThrow to handle errors in a typesafe manner.
While SafeFn really shines when used together with NeverThrow, incremental adoption is easy as the only requirement is that your handler returns a Result
created through NeverThrow's ok()
or err()
.
Besides the typesafety, SafeFn also aims to improve on existing solutions by allowing for flexible composition and configuration of functions.
NeverThrow
For a quick introduction to NeverThrow, check out this Twitter thread by Matt Pocock. Their docs can be found here.
Put very briefly, NeverThrow provides a Result
type which enables typed errors. A result can be created through the ok()
and err()
functions, and can be consumed through result.value
and result.error
.
Quick SafeFn examples
Take the simple case of a function that creates a todo for a user (very original I know). We first need to make sure the user is signed in, then validate the input of the todo, store it in the database if it's valid and then return it.
All of this needs to be done while taking care of errors that might occur in any of these steps.
Considering both of these functions can throw we should probably add some error handling as such:
This results in a fully typed function with the following return type:
we can easily call this function using the run()
method:
or on the client by creating an action and calling it with useServerAction()
, in this example we're adding a platform specific callback to handle revalidation:
However, the magic of the integration of NeverThrow really comes out if your other functions are also returning a Result
or ResultAsync
. Instead of passing a regular function via handler()
you can also use safeHandler()
.
This builds on Neverthrow's safeTry()
and takes in a generator function. This generator function receives the same args as handler()
, but it allows you to yield *
other results (note that in Neverthrow
versions before 8.0.0 you need to use .safeUnwrap()
).
This is meant to emulate Rust's ?
operator and allows a very ergonomic way to write "return early if this fails, otherwise continue" logic.
This function has the same return type as the handler()
example above.