Error Types
FirestoreORM provides specialized error classes for common failure scenarios.ValidationError
Thrown when Zod schema validation fails during create or update operations.import { ValidationError } from '@spacelabstech/firestoreorm';
try {
await userRepo.create({
name: '', // Too short
email: 'not-email', // Invalid format
age: -5 // Negative number
});
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed:');
error.issues.forEach(issue => {
console.log(`${issue.path.join('.')}: ${issue.message}`);
});
// Output:
// name: String must contain at least 1 character(s)
// email: Invalid email address
// age: Number must be greater than 0
}
}
When it occurs:
- During
create()- validates against full schema - During
update()- validates againstschema.partial()(all fields optional) - Before any Firestore write occurs (no wasted operations)
NotFoundError
Thrown when a requested document doesn’t exist.import { NotFoundError } from '@spacelabstech/firestoreorm';
try {
const user = await userRepo.getById('non-existent-id');
if (!user) {
throw new NotFoundError('User not found');
}
// Or let update() throw it
await userRepo.update('non-existent-id', { name: 'John' });
} catch (error) {
if (error instanceof NotFoundError) {
console.log('Document does not exist');
// Return 404 to client
}
}
ConflictError
Thrown when an operation conflicts with existing data or business rules.import { ConflictError } from '@spacelabstech/firestoreorm';
// Check uniqueness before creation
tenantRepo.on('beforeCreate', async (tenant) => {
const existing = await tenantRepo.findByField('slug', tenant.slug);
if (existing.length > 0) {
throw new ConflictError(`Tenant with slug '${tenant.slug}' already exists`);
}
});
try {
await tenantRepo.create({
name: 'Acme Corp',
slug: 'acme', // Already exists
// ...
});
} catch (error) {
if (error instanceof ConflictError) {
console.log('Duplicate slug detected');
// Return 409 to client
}
}
Use
ConflictError for:- Duplicate unique fields (email, username, slug)
- Business rule violations (insufficient funds, seat limits)
- State conflicts (can’t ship a cancelled order)
FirestoreIndexError
Thrown when a query requires a composite index that doesn’t exist.import { FirestoreIndexError } from '@spacelabstech/firestoreorm';
try {
const results = await orderRepo.query()
.where('status', '==', 'pending')
.where('total', '>', 100)
.orderBy('createdAt', 'desc')
.get();
} catch (error) {
if (error instanceof FirestoreIndexError) {
// Error includes link to create the index
console.log(error.toString());
// "Missing composite index for collection 'orders'.
// Fields: status (==), total (>), createdAt (desc)
// Create index: https://console.firebase.google.com/..."
console.log('Index URL:', error.indexUrl);
console.log('Required fields:', error.fields);
}
}
Development vs Production:Development: Click the URL in the error message, create the index in Firebase Console, wait 1-2 minutes, then retry.Production: Create indexes before deployment using:
firestore.indexes.jsonfile- Firebase CLI:
firebase deploy --only firestore:indexes - CI/CD pipeline automation
Express.js Error Handling
Using the Built-in Error Handler
FirestoreORM includes a pre-built Express middleware that automatically maps errors to HTTP status codes.app.ts
import express from 'express';
import { errorHandler } from '@spacelabstech/firestoreorm';
import userRoutes from './routes/user.routes';
const app = express();
app.use(express.json());
app.use('/api', userRoutes);
// Register error handler as LAST middleware
app.use(errorHandler);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
| Error Type | HTTP Status | Response |
|---|---|---|
ValidationError | 400 Bad Request | { error: 'Validation Error', details: [...] } |
NotFoundError | 404 Not Found | { error: 'Not Found', message: '...' } |
ConflictError | 409 Conflict | { error: 'Conflict', message: '...' } |
FirestoreIndexError | 400 Bad Request | { error: 'Index Required', indexUrl: '...' } |
| Others | 500 Internal Server Error | { error: 'Internal Server Error' } |
Route Error Handling
routes/user.routes.ts
import express from 'express';
import { userRepo } from '../repositories/user.repository';
import { NotFoundError } from '@spacelabstech/firestoreorm';
const router = express.Router();
// Create user
router.post('/users', async (req, res, next) => {
try {
const user = await userRepo.create({
...req.body,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
res.status(201).json(user);
} catch (error) {
next(error); // errorHandler middleware processes this
}
});
// Get user by ID
router.get('/users/:id', async (req, res, next) => {
try {
const user = await userRepo.getById(req.params.id);
if (!user) {
throw new NotFoundError(`User ${req.params.id} not found`);
}
res.json(user);
} catch (error) {
next(error);
}
});
// Update user
router.patch('/users/:id', async (req, res, next) => {
try {
const user = await userRepo.update(req.params.id, {
...req.body,
updatedAt: new Date().toISOString()
});
res.json(user);
} catch (error) {
next(error);
}
});
export default router;
Always pass errors to
next(error) instead of handling them directly in routes. This ensures consistent error responses across your API.NestJS Error Handling
Custom Exception Filter
filters/firestore-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus
} from '@nestjs/common';
import { Response } from 'express';
import {
ValidationError,
NotFoundError,
ConflictError,
FirestoreIndexError
} from '@spacelabstech/firestoreorm';
@Catch(ValidationError, NotFoundError, ConflictError, FirestoreIndexError)
export class FirestoreExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
if (exception instanceof ValidationError) {
response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
error: 'Validation Error',
details: exception.issues
});
} else if (exception instanceof NotFoundError) {
response.status(HttpStatus.NOT_FOUND).json({
statusCode: HttpStatus.NOT_FOUND,
error: 'Not Found',
message: exception.message
});
} else if (exception instanceof ConflictError) {
response.status(HttpStatus.CONFLICT).json({
statusCode: HttpStatus.CONFLICT,
error: 'Conflict',
message: exception.message
});
} else if (exception instanceof FirestoreIndexError) {
response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
error: 'Index Required',
message: exception.message,
indexUrl: exception.indexUrl,
fields: exception.fields
});
}
}
}
Register Filter Globally
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { FirestoreExceptionFilter } from './filters/firestore-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Register global exception filter
app.useGlobalFilters(new FirestoreExceptionFilter());
await app.listen(3000);
}
bootstrap();
Service Layer Error Handling
modules/user/user.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { NotFoundError, ConflictError } from '@spacelabstech/firestoreorm';
@Injectable()
export class UserService {
constructor(private userRepository: UserRepository) {}
async findOne(id: string) {
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async update(id: string, dto: UpdateUserDto) {
try {
return await this.userRepository.update(id, dto);
} catch (error) {
if (error instanceof NotFoundError) {
throw new NotFoundException(error.message);
}
throw error;
}
}
async createWithUniqueEmail(dto: CreateUserDto) {
// Check for duplicate email
const existing = await this.userRepository
.query()
.where('email', '==', dto.email)
.getOne();
if (existing) {
throw new ConflictError(`Email ${dto.email} is already registered`);
}
return this.userRepository.create(dto);
}
}
Validation Error Handling
Detailed Validation Messages
import { ValidationError } from '@spacelabstech/firestoreorm';
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email format'),
age: z.number().int().positive('Age must be a positive number').optional(),
password: z.string().min(8, 'Password must be at least 8 characters')
});
try {
await userRepo.create({
name: 'A',
email: 'invalid',
age: -1,
password: 'short'
});
} catch (error) {
if (error instanceof ValidationError) {
// Format for API response
const formattedErrors = error.issues.reduce((acc, issue) => {
const field = issue.path.join('.');
acc[field] = issue.message;
return acc;
}, {} as Record<string, string>);
console.log(formattedErrors);
// {
// name: 'Name must be at least 2 characters',
// email: 'Invalid email format',
// age: 'Age must be a positive number',
// password: 'Password must be at least 8 characters'
// }
}
}
Partial Update Validation
// Updates validate against schema.partial() - all fields optional
try {
await userRepo.update('user-123', {
email: 'invalid-email' // Only this field is validated
});
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.issues);
// [{ path: ['email'], message: 'Invalid email format' }]
}
}
// Valid partial update
await userRepo.update('user-123', {
name: 'Updated Name' // Other fields not required
});
Transaction Error Handling
Transaction Failures
import { ConflictError } from '@spacelabstech/firestoreorm';
try {
await accountRepo.runInTransaction(async (tx, repo) => {
const from = await repo.getForUpdate(tx, fromAccountId);
const to = await repo.getForUpdate(tx, toAccountId);
if (!from || !to) {
throw new NotFoundError('Account not found');
}
if (from.balance < amount) {
throw new ConflictError('Insufficient funds');
}
await repo.updateInTransaction(tx, fromAccountId, {
balance: from.balance - amount
});
await repo.updateInTransaction(tx, toAccountId, {
balance: to.balance + amount
});
});
} catch (error) {
if (error instanceof ConflictError) {
console.log('Transfer failed:', error.message);
} else if (error instanceof NotFoundError) {
console.log('Invalid account:', error.message);
} else {
console.log('Transaction failed:', error);
}
}
Transaction Rollback: If any error is thrown inside a transaction, Firestore automatically rolls back all changes. Make sure to throw errors for invalid states to prevent partial updates.
Hook-Based Validation
Business Rule Validation
// Validate before operations
orderRepo.on('beforeUpdate', (data) => {
if (data.status === 'shipped' && !data.trackingNumber) {
throw new ValidationError({
issues: [{
path: ['trackingNumber'],
message: 'Tracking number required for shipped orders'
}]
});
}
});
orderRepo.on('beforeCreate', async (order) => {
// Check inventory
for (const item of order.items) {
const available = await inventoryService.checkStock(
item.productId,
item.quantity
);
if (!available) {
throw new ConflictError(
`Insufficient stock for ${item.productName}`
);
}
}
});
Uniqueness Validation
tenantRepo.on('beforeCreate', async (tenant) => {
const existing = await tenantRepo.findByField('slug', tenant.slug);
if (existing.length > 0) {
throw new ConflictError(
`Tenant with slug '${tenant.slug}' already exists`
);
}
});
userRepo.on('beforeCreate', async (user) => {
const existing = await userRepo
.query()
.where('email', '==', user.email)
.getOne();
if (existing) {
throw new ConflictError(
`Email ${user.email} is already registered`
);
}
});
Use
beforeCreate and beforeUpdate hooks for validation that requires database queries. This keeps validation logic centralized and ensures it runs for all create/update operations.Logging and Monitoring
Error Logging with Context
import { logger } from './logger';
try {
await userRepo.update(userId, updateData);
} catch (error) {
// Log with context
logger.error('Failed to update user', {
userId,
updateData,
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString()
});
// Re-throw for upstream handling
throw error;
}
Centralized Error Tracking
import * as Sentry from '@sentry/node';
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
// Log to Sentry for all errors
Sentry.captureException(err, {
tags: {
errorType: err.constructor.name,
route: req.path
},
extra: {
body: req.body,
params: req.params,
query: req.query
}
});
// Then handle with FirestoreORM error handler
errorHandler(err, req, res, next);
});
Testing Error Scenarios
Unit Testing Errors
import { ValidationError, NotFoundError } from '@spacelabstech/firestoreorm';
describe('UserService', () => {
it('should throw ValidationError for invalid email', async () => {
await expect(
userService.create({
name: 'John',
email: 'invalid-email'
})
).rejects.toThrow(ValidationError);
});
it('should throw NotFoundError when user does not exist', async () => {
await expect(
userService.findOne('non-existent-id')
).rejects.toThrow(NotFoundError);
});
it('should throw ConflictError for duplicate email', async () => {
await userService.create({
name: 'John',
email: '[email protected]'
});
await expect(
userService.create({
name: 'Jane',
email: '[email protected]'
})
).rejects.toThrow(ConflictError);
});
});