Error handling
Orchid ORM
and a query builder pqb
can throw errors of two classes (that you can import from 'orchid-orm'):
OrchidOrmError
- can be exposed to user, for now only NotFoundError
extends it.
OrchidOrmInternalError
- should not be exposed, has several sub-classes:
QueryError
- wraps a database error, see below.MoreThanOneRowError
- may be thrown byupsert
andorCreate
methods, as they expect 0 or 1 record to be found.UnhandledTypeError
- internal error that must never happen, indicates a bug in the library.
global error handling
When using find
, findBy
, take
, get
, the ORM will throw NotFoundError
in case when record is not found.
This is the only ORM error that can be safely exposed to users.
Here is how centralized error handler may look like:
import { ZodError } from 'zod';
import { ValiError } from 'valibot';
import { NotFoundError } from 'orchid-orm';
// generic error class that the code of your app will use to throw errors
export class AppError extends Error {
constructor(message?: string) {
super(message);
}
}
// more specific error classes extends AppError
export class SomeSpecificError extends AppError {
message = 'some specific error happened';
}
export const performSomeAction = () => {
// when the error can be exposed to user, use AppError
throw new AppError('Oops');
// otherwise, a standard Error
throw new Error('Internal error');
};
// express.js error handler
app.use((err, req, res, next) => {
// log the error
console.error(err);
// instanceof AppError means that it can be exposed to user
if (err instanceof AppError) {
// client never cares about error status, let it be 400 for all kinds of AppError
return res.status(400).send({
error: err.message,
});
}
// default message is: Record is not found
if (err instanceof NotFoundError) {
return res.status(400).send({
error: err.message,
});
}
// catch Zod errors
if (err instanceof ZodError) {
return res.status(400).send({
// serialize validation error somehow
});
}
if (err instanceof ValiError) {
return res.status(400).send({
error: err.issues.map((iss) => iss.message).join('. '),
});
}
res.status(500).send('Something broke!');
});
INFO
Hint for express.js users:
It still doesn't support async error handling, so you have to install a package like this, or come up with a custom helper/wrapper to catch errors of async routes, or to write boilerplative try-catch in every route.
Or switch to a modern framework 😅
database error
Stack trace of query errors is pointing to the library internals, in addition they have a property cause
- it is a nested error with a stack trace pointing to the place in your code that started the query.
Error class has the same properties as in error of the pg
module and some additional properties described below.
All errors thrown when performing queries are wrapped in the error class specific to the concrete table.
And all tables have a property error
that you can use to determine if the error belongs to the table.
Imagine we are going to save a new user, and want to handle possible uniqueness violations.
We can perform 4 database queries for this:
- begin transaction (to avoid race conditions)
- query if such user already exists
- save a user
- end transaction
Or, instead, just one query is enough, we only need to handle the error:
try {
await db.table.create(...data);
} catch (error) {
if (error instanceof db.table.error) {
// `isUnique` in case of a unique violation error,
// when the value of the unique column already exists
if (error.isUnique) {
// columns have type { [column name]?: true }
// use it to determine which columns have failed uniqueness
if (error.columns.username) {
throw new Error('Username is already taken');
}
if (error.columns.email) {
throw new Error('Email is already taken');
}
}
}
// rethrow the error if it is not recognized
throw error;
}
Error classes on the table interface are extending the common QueryError
, it has all the same properties as DatabaseError
from pg
.