Error Handling With Result Pattern
The ordinary way we’ve been taught is to throw exceptions. This simply applies the fail-fast principle. When an error occurs in the code, an exception is thrown, which immediately stops the method’s execution and passes the responsibility of handling the exception to the caller.
The issue is that the caller needs to be aware of which exceptions to handle, but this information isn’t clear from just looking at the method signature. Another frequent scenario is using exceptions to indicate validation errors.
For instance, you may see code like this:
1class FollwerService {2private followerRepository: IFollowerRepository;34// ...56public async startFollowingAsync(7user: User,8followed: User,9createdOn: Date,10): Promise<void> {11if (user.id === followed.id) {12throw new DomainException("Can't follow yourself");13}1415if (!followed.hasPublicProfile) {16throw new DomainException("Can't follow a non-public profile");17}1819const isAlreadyFollowing = await this.followerRepository.isAlreadyFollowingAsync(20user.id,21followed.id,22);2324if (isAlreadyFollowing) {25throw new DomainException('Already following');26}2728// Create a new follower and insert it into the repository29const follower = Follower.create(user.id, followed.id, createdOn);3031this.followerRepository.insert(follower);32}33}
At Microsoft, something I learned was to handle results properly especially when we have huge codebases, and knowing how to handle these errors across the systems was crucial for observability purposes.
Let’s see a common example where we might distinguish results are when we want to perform API calls. Let’s see how this might be used in a fetch utility:
1enum ResultType {2Success,3Failure,4}56type Result<TResult, TError> =7| { type: ResultType.Success; value: TResult }8| { type: ResultType.Failure; error: TError };910export type AsyncResult<TResult, TError = unknown> =11| Result<TResult, TError>12| { type: "inProgress" }; // Optional1314export const getData = async <T>(15url: string16): Promise<AsyncResult<T, string>> => {17try {18const response = await fetch(url);19const data = (await response.json()) as T;2021return {22type: ResultType.Success,23value: data,24};25} catch (error) {26return {27type: ResultType.Failure,28error: `An error occured: ${(error as Error).message}`,29};30}31};
Depending on your needs, you may modify the Result
to your scenario and add necessary information.
Take a look at the code and understand the basic intuitive approach of this pattern.
Let’s see how such similar use case may fit:
Consider the Result Types
1interface AsyncInProgress {2type: ResultType.inProgress;3}45interface AsyncSuccess<TResult> {6type: ResultType.Success;7value: TResult;8}910interface AsyncFailure<TError> {11type: ResultType.Failure;12error: TError;13}1415enum ResultType {16Success,17Failure,18inProgress,19}2021type Result<TResult, TError> =22| { type: ResultType.Success; value: TResult }23| { type: ResultType.Failure; error: TError };2425type AsyncResult<TResult, TError = unknown> =26| Result<TResult, TError>27| { type: ResultType.inProgress }; // optional
As seen before, we might use result types like this:
1export const getData = async <T>(2url: string3): Promise<AsyncResult<T, string>> => {4try {5const response = await fetch(url);6const data = (await response.json()) as T;78return {9type: ResultType.Success,10value: data,11};12} catch (error) {13return {14type: ResultType.Failure,15error: `An error occured: ${(error as Error).message}`,16};17}18};
A nice approach is that we can create a seperate useResult()
hook:
1export const useGetResult = <TResult, TError = unknown>(2url: string3): AsyncResult<TResult, TError> => {4const [result, setResult] =5useState<AsyncResult<TResult, TError>>(ASYNC_IN_PROGRESS);67useEffect(() => {8const fetchData = async () => {9try {10const response = await fetch(url, { method: "GET" });11const data = (await response.json()) as TResult;12setResult(asAsyncSuccess(data));13} catch (error) {14setResult(asAsyncFailure(error as TError));15}16};17fetchData();18}, [url]);19return result;20};
If you’ve used anything related to state management such as useQuery()
this might seem
like a similar approach. I personally find myself using useQuery()
in client side code or if
I’m using Redux sagas then I will leverage the initial state. if I’m writing server-side code, I’ll be writing my custom Results.
1export const UserDashboard = () => {2const userResult = useGetResult<User[]>("...");34if (userResult.type === ResultType.inProgress) {5return <span>Loading...</span>;6}78if (userResult.type === ResultType.Failure) {9return // ...10}1112// sucesss case ....1314};
That being said, this was an introduction to using the Result Pattern. I hope you learned something, as well, considering in applying similar practices to your projects.