Lifecycle Hooks
beforeInvoke, afterInvoke, onSuccess, onError
Lifecycle Hooks allow you to run code at different stages of query/mutation execution - perfect for logging, metrics, validation, and more.
Available Hooks
beforeInvoke
Runs before the handler executes:
const getUser = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
return success(await ctx.db.find(args.id));
},
})
.beforeInvoke(async (ctx, args) => {
console.log(`Fetching user ${args.id}`);
});afterInvoke
Runs after the handler completes (always runs):
const createUser = t.mutation({
args: z.object({ name: z.string() }),
handler: async (ctx, args) => {
return success(await ctx.db.create(args));
},
})
.afterInvoke(async (ctx, args, result) => {
// Runs regardless of success or failure
console.log(`Create user completed:`, result.isSuccess());
});onSuccess
Runs only on success:
const deleteUser = t.mutation({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
await ctx.db.delete(args.id);
return success({ id: args.id });
},
})
.onSuccess(async (ctx, args, data) => {
// Send notification only if deletion succeeded
await ctx.emailService.send(args.id, "Your account has been deleted");
});onError
Runs only on error:
const updatePost = t.mutation({
args: z.object({ id: z.number(), title: z.string() }),
handler: async (ctx, args) => {
return success(await ctx.db.update(args.id, args));
},
})
.onError(async (ctx, args, error) => {
// Log errors
await ctx.logger.error("Failed to update post", { error, args });
});Execution Order
Hooks execute in this order:
- beforeInvoke - Before handler
- Handler - Your main logic
- onSuccess OR onError - Based on result
- afterInvoke - Always runs at the end
const procedure = t.mutation({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
console.log("2. Handler executing");
return success({ id: args.id });
},
})
.beforeInvoke(async (ctx, args) => {
console.log("1. Before invoke");
})
.onSuccess(async (ctx, args, data) => {
console.log("3. On success");
})
.onError(async (ctx, args, error) => {
console.log("3. On error");
})
.afterInvoke(async (ctx, args, result) => {
console.log("4. After invoke");
});Chaining Hooks
Chain multiple hooks of the same type:
const createUser = t.mutation({
args: z.object({ name: z.string() }),
handler: async (ctx, args) => {
return success(await ctx.db.create(args));
},
})
.beforeInvoke(async (ctx, args) => {
console.log("Starting user creation");
})
.beforeInvoke(async (ctx, args) => {
await validateRateLimit(ctx.userId);
})
.onSuccess(async (ctx, args, data) => {
await ctx.cache.invalidate("users");
})
.onSuccess(async (ctx, args, data) => {
await ctx.analytics.track("user_created", { userId: data.id });
});Use Cases
Logging
const query = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
return success(await ctx.db.find(args.id));
},
})
.beforeInvoke(async (ctx, args) => {
ctx.logger.info("Query starting", { args });
})
.afterInvoke(async (ctx, args, result) => {
ctx.logger.info("Query completed", {
success: result.isSuccess(),
duration: Date.now() - startTime,
});
});Metrics
const mutation = t.mutation({
args: z.object({ name: z.string() }),
handler: async (ctx, args) => {
return success(await ctx.db.create(args));
},
})
.beforeInvoke(async (ctx, args) => {
ctx.metrics.startTimer("mutation_duration");
})
.afterInvoke(async (ctx, args, result) => {
ctx.metrics.record("mutation_duration");
ctx.metrics.increment("mutations_total", {
success: result.isSuccess(),
});
});Validation
const updatePost = t.mutation({
args: z.object({
id: z.number(),
title: z.string(),
}),
handler: async (ctx, args) => {
return success(await ctx.db.update(args.id, args));
},
})
.beforeInvoke(async (ctx, args) => {
// Custom validation before handler
const post = await ctx.db.find(args.id);
if (!post) {
throw new Error("Post not found");
}
if (post.authorId !== ctx.userId) {
throw new Error("Unauthorized");
}
});Caching
const getUser = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
return success(await ctx.db.find(args.id));
},
})
.beforeInvoke(async (ctx, args) => {
// Check cache first
const cached = await ctx.cache.get(`user:${args.id}`);
if (cached) {
return success(cached); // Won't execute handler
}
})
.onSuccess(async (ctx, args, data) => {
// Cache successful result
await ctx.cache.set(`user:${args.id}`, data, 300);
});Error Handling
const deleteAccount = t.mutation({
args: z.object({ userId: z.number() }),
handler: async (ctx, args) => {
await ctx.db.delete(args.userId);
return success(undefined);
},
})
.onError(async (ctx, args, error) => {
// Send alert on error
await ctx.alerts.send({
severity: "high",
message: "Account deletion failed",
error: error.message,
userId: args.userId,
});
})
.afterInvoke(async (ctx, args, result) => {
// Log regardless of outcome
await ctx.logger.audit("account_deletion_attempt", {
userId: args.userId,
success: result.isSuccess(),
});
});Best Practices
1. Use Hooks for Cross-Cutting Concerns
// ✅ Good - hooks for cross-cutting logic
const procedure = t.mutation({
handler: async (ctx, args) => {
// Business logic only
return success(await ctx.db.create(args));
},
})
.beforeInvoke(logStart)
.beforeInvoke(checkRateLimit)
.afterInvoke(logEnd)
.onSuccess(invalidateCache);
// ❌ Bad - mixing concerns in handler
const procedure = t.mutation({
handler: async (ctx, args) => {
logStart();
checkRateLimit();
const result = await ctx.db.create(args);
invalidateCache();
logEnd();
return success(result);
},
});2. Keep Hooks Simple
// ✅ Good - simple, focused hooks
.beforeInvoke(async (ctx, args) => {
await validateRateLimit(ctx.userId);
})
// ❌ Bad - complex logic in hooks
.beforeInvoke(async (ctx, args) => {
const rateLimit = await ctx.redis.get(`rate:${ctx.userId}`);
const user = await ctx.db.find(ctx.userId);
const settings = await ctx.db.getSettings(user.plan);
if (rateLimit > settings.limit) {
throw new Error("Rate limit exceeded");
}
// ... 20 more lines
})3. Don't Modify Args in Hooks
// ❌ Bad - mutating args
.beforeInvoke((ctx, args) => {
args.timestamp = Date.now(); // Mutation!
})
// ✅ Good - use context
.beforeInvoke((ctx, args) => {
ctx.metadata.startTime = Date.now();
})See Also
- Context System - Hooks receive context
- Queries & Mutations - Defining procedures
- Authorization & Checks - Pre-execution validation