Skip to content

Error Handling

Our backend code generally avoids throwing errors, instead returns objects called “Results”. Results objects will always contain either the desired value or one of the possible error types specified by the Result signature.

From Wikipedia:

In functional programming, a result type is a monadic type holding a returned value or an error code. They provide an elegant way of handling errors, without resorting to exception handling; when a function that may fail returns a result type, the programmer is forced to consider success or failure paths, before getting access to the expected result; this eliminates the possibility of an erroneous programmer assumption.

An operation to look up a user record in the database might resolve in one of three ways:

  1. Succeed and return a User
  2. Fail when the record isn’t found
  3. Fail with some other unspecified server error.

Here’s how we might model that in our server-side code:

Result Example
import { R } from '@mobily/ts-belt'
import { NotFoundError, UnknownError } from '#app/utils/errors.server'
interface User {
name: string
age: number
}
function fetchUserById(
id: string,
):R.Result<User, NotFoundError | UnknownError> {
try {
// fake database lookup
if (id === '123') {
return R.Ok({ name: 'Blarg Blargerson', age: 42 })
} else {
return R.Error(new NotFoundError('user not found'))
}
} catch (err) {
return R.Error(new UnknownError('something bad happened'))
}
}
const userResult = fetchUserById('123')
if (R.isOk(userResult)) {
console.log(`user is ${userResult._0.age} years old.}`)
} else {
console.log(`something went wrong! -- ${userResult._0.message}`)
}

Custom Error classes extend the standard Error and include a name that matches their class name and a statusCode property:

Custom Errors - #app/utils/errors.server.ts
...
export class NotFoundError extends CustomError {
public statusCode: number
constructor(message: string) {
super(message)
this.name = 'NotFoundError'
this.statusCode = HttpStatusCode.NOT_FOUND
}
}
export class TokenValidationError extends CustomError {
public statusCode: number
constructor(message: string) {
super(message)
this.name = 'TokenValidationError'
this.statusCode = HttpStatusCode.UNAUTHORIZED
}
}
...

When returning an error to the API client, this function returns either a CustomError class or ZodError:

Custom Errors - #app/utils/api.server.ts
...
export const jsonFromErrorResult = (
result: R.Error<CustomError | ZodError>,
): Record<string, unknown> => {
let originalError
if ('statusCode' in result._0) {
originalError = result._0 as CustomError
return json(
{ message: originalError.message, name: originalError.name },
originalError.statusCode,
)
} else {
originalError = result._0 as ZodError
return json(
{ issues: originalError.issues, name: originalError.name },
HttpStatusCode.BAD_REQUEST,
)
}
}
...