Blogs
Result Pattern

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:

Typescript
1
class FollwerService {
2
private followerRepository: IFollowerRepository;
3
4
// ...
5
6
public async startFollowingAsync(
7
user: User,
8
followed: User,
9
createdOn: Date,
10
): Promise<void> {
11
if (user.id === followed.id) {
12
throw new DomainException("Can't follow yourself");
13
}
14
15
if (!followed.hasPublicProfile) {
16
throw new DomainException("Can't follow a non-public profile");
17
}
18
19
const isAlreadyFollowing = await this.followerRepository.isAlreadyFollowingAsync(
20
user.id,
21
followed.id,
22
);
23
24
if (isAlreadyFollowing) {
25
throw new DomainException('Already following');
26
}
27
28
// Create a new follower and insert it into the repository
29
const follower = Follower.create(user.id, followed.id, createdOn);
30
31
this.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:

Typescript
1
enum ResultType {
2
Success,
3
Failure,
4
}
5
6
type Result<TResult, TError> =
7
| { type: ResultType.Success; value: TResult }
8
| { type: ResultType.Failure; error: TError };
9
10
export type AsyncResult<TResult, TError = unknown> =
11
| Result<TResult, TError>
12
| { type: "inProgress" }; // Optional
13
14
export const getData = async <T>(
15
url: string
16
): Promise<AsyncResult<T, string>> => {
17
try {
18
const response = await fetch(url);
19
const data = (await response.json()) as T;
20
21
return {
22
type: ResultType.Success,
23
value: data,
24
};
25
} catch (error) {
26
return {
27
type: ResultType.Failure,
28
error: `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

Typescript
1
interface AsyncInProgress {
2
type: ResultType.inProgress;
3
}
4
5
interface AsyncSuccess<TResult> {
6
type: ResultType.Success;
7
value: TResult;
8
}
9
10
interface AsyncFailure<TError> {
11
type: ResultType.Failure;
12
error: TError;
13
}
14
15
enum ResultType {
16
Success,
17
Failure,
18
inProgress,
19
}
20
21
type Result<TResult, TError> =
22
| { type: ResultType.Success; value: TResult }
23
| { type: ResultType.Failure; error: TError };
24
25
type AsyncResult<TResult, TError = unknown> =
26
| Result<TResult, TError>
27
| { type: ResultType.inProgress }; // optional

As seen before, we might use result types like this:

Typescript
1
export const getData = async <T>(
2
url: string
3
): Promise<AsyncResult<T, string>> => {
4
try {
5
const response = await fetch(url);
6
const data = (await response.json()) as T;
7
8
return {
9
type: ResultType.Success,
10
value: data,
11
};
12
} catch (error) {
13
return {
14
type: ResultType.Failure,
15
error: `An error occured: ${(error as Error).message}`,
16
};
17
}
18
};

A nice approach is that we can create a seperate useResult() hook:

useGetResult.ts
1
export const useGetResult = <TResult, TError = unknown>(
2
url: string
3
): AsyncResult<TResult, TError> => {
4
const [result, setResult] =
5
useState<AsyncResult<TResult, TError>>(ASYNC_IN_PROGRESS);
6
7
useEffect(() => {
8
const fetchData = async () => {
9
try {
10
const response = await fetch(url, { method: "GET" });
11
const data = (await response.json()) as TResult;
12
setResult(asAsyncSuccess(data));
13
} catch (error) {
14
setResult(asAsyncFailure(error as TError));
15
}
16
};
17
fetchData();
18
}, [url]);
19
return 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.

user-dashboard.component.tsx
1
export const UserDashboard = () => {
2
const userResult = useGetResult<User[]>("...");
3
4
if (userResult.type === ResultType.inProgress) {
5
return <span>Loading...</span>;
6
}
7
8
if (userResult.type === ResultType.Failure) {
9
return // ...
10
}
11
12
// sucesss case ....
13
14
};

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.