Core concepts

Error handling

Nullable results vs throwing, mapping errors at the boundary, and cross-dialect constraint detection helpers.

better-drizzle keeps nullability and exceptional flow separate, and ships helpers to classify database errors consistently across PostgreSQL, SQLite, and MySQL.

Nullable at the boundary

Let the type carry the "might not exist" and decide at the edge:

const user = await client.users.findUnique({
	where: { email: 'alice@example.com' },
});

if (!user) return null;
return user;

Throw inside the service

When not-found is exceptional, .throw() keeps the happy path linear:

const user = await client.users
	.findUnique({ where: { email: 'alice@example.com' } })
	.throw(() => new Error('User not found'));

Map errors at the protocol boundary

Keep HTTP/RPC mapping close to the layer that understands it:

app.get('/users/:id', async (req, res) => {
	try {
		const user = await client.users
			.findOne({ where: { id: Number(req.params.id) } })
			.throw(() => new Error('Not found'));

		res.json(user);
	} catch (error) {
		res.status(404).json({ error: String(error) });
	}
});

Classifying database errors

Writes fail in predictable ways — a unique email, a missing foreign key, a NOT NULL violation. Each driver reports these differently. better-drizzle normalizes them with exported helpers so your handling is dialect-agnostic:

import {
	isUniqueViolation,
	isForeignKeyViolation,
	isNotNullViolation,
	isCheckViolation,
	getDatabaseErrorInfo,
} from 'better-drizzle';

try {
	await client.users.create({
		data: { email: 'taken@example.com', name: 'Dup', active: true },
	});
} catch (error) {
	if (isUniqueViolation(error)) {
		return { status: 409, message: 'Email already in use' };
	}
	throw error;
}

Each predicate accepts an optional constraint (or column) name to match a specific one:

if (isUniqueViolation(error, 'users_email_unique')) {
	// only the email uniqueness constraint
}

if (isNotNullViolation(error, 'name')) {
	// only the name column
}
HelperDetectsPG · SQLite · MySQL
isUniqueViolationduplicate unique / primary key23505 · SQLITE_CONSTRAINT_UNIQUE · 1062
isForeignKeyViolationmissing / referenced FK23503 · SQLITE_CONSTRAINT_FOREIGNKEY · 1451/1452
isNotNullViolationNOT NULL violated23502 · SQLITE_CONSTRAINT_NOTNULL · 1048
isCheckViolationCHECK violated23514 · SQLITE_CONSTRAINT_CHECK · 3819

Inspecting the details

For logging or custom handling, getDatabaseErrorInfo returns a normalized object regardless of driver:

const info = getDatabaseErrorInfo(error);
//    ^? { driver, code, errno, constraint, table, column, message }

logger.warn('db error', {
	driver: info.driver,
	code: info.code,
	constraint: info.constraint,
});

There is also isDatabaseError(error) to guard before drilling in.

Transaction failures

Inside a transaction, throwing or calling tx.rollback() aborts the whole unit:

await client.transaction(async (tx) => {
	if (await tx.users.exists({ where: { email } })) {
		tx.rollback('duplicate email');
	}

	await tx.users.create({ data: { email, name: 'Alice', active: true } });
});

Rules of thumb

  • Keep validation close to request parsing.
  • Keep not-found mapping close to the boundary that understands the protocol.
  • Use the constraint helpers instead of string-matching driver error messages by hand.

On this page