FeathersJS Service Hooks Not Executing: Diagnostic Guide
Diagnose why FeathersJS service hooks (before/after/error) don't run: check registration path, async context returns, transport differences, and direct database bypasses. Includes ordered checks, version-specific fixes, and escalation criteria.
11 Aug 2025, 04:18 UTC

Recognizable Condition
Your FeathersJS service methods (create, update, patch, remove, find) complete and return data, but registered before/after/error hooks do not run, run out of order, or silently fail without throwing. The service responds normally, yet logging, validation, or side-effect logic inside hooks never executes.
Cause and Diagnostic Quick-Reference
| Symptom | Likely Cause | Quick Test |
|---|---|---|
| Hooks never fire on any transport | Hooks bound to wrong service path or instance | Check registration path matches app.service('exact-path') |
| First async hook runs, subsequent hooks skipped | Async hook missing return or await on context | Add log at start/end of each hook; observe where chain stops |
| Hooks fire on REST but not Socket.io (or vice versa) | Transport-specific middleware bypassing service layer | Call same method via Feathers client over both transports |
| Hooks fire via client but not from internal code | Direct database adapter calls (Model.find()) skipping service | Search codebase for direct model usage instead of service.find() |
Ordered Diagnostic Checks
Check 1: Verify Hook Registration Timing and Path
Run this check in your application bootstrap code (typically app.js or a service configuration file). Confirm:
- The service is initialized via
app.use('/path', new MyService())before hooks are registered. - Hook registration uses the exact same path string including leading slash:
app.service('/path').hooks({ before: { create: [myHook] } }). - You are using the hook registration syntax for your Feathers major version. In v4,
app.service('path').hooks({...}); in v5,app.service('path').hooks({...})works but the recommended pattern isservice.hooks({...})on the service class itself.
Risk: Registering hooks before app.use() attaches them to a non-existent service, silently creating a new empty service instance.
Check 2: Add Execution Logging Inside the Suspect Hook
Edit the hook file directly. Insert a log at the very first line:
// my-hook.js
module.exports = () => async context => {
console.log('[HOOK ENTER]', context.type, context.method, context.path);
// ... existing logic
console.log('[HOOK EXIT]', context.type, context.method);
return context;
};
Invoke the service method through the Feathers client (not direct HTTP fetch) so the full transport stack runs:
// client test
const client = feathers();
client.configure(feathers.rest(baseUrl).fetch(fetch));
await client.service('path').create({ test: true });
Observe console output. If [HOOK ENTER] never appears, the hook is not in the chain. If ENTER appears but EXIT does not, the hook is short-circuiting (see Check 3).
Check 3: Test Across REST and Real-Time Transports
Run the same service call through both transports in quick succession:
// REST
const restClient = feathers();
restClient.configure(feathers.rest(baseUrl).fetch(fetch));
await restClient.service('path').create({ via: 'rest' });
// Socket.io
const socketClient = feathers();
socketClient.configure(feathers.socketio(io()));
await socketClient.service('path').create({ via: 'socketio' });
If hooks fire on one transport but not the other, inspect transport-specific middleware (e.g., express.json() ordering, Socket.io authentication middleware) that may terminate the request before reaching the service layer.
Check 4: Search for Direct Database Adapter Bypass
Grep your codebase for direct model usage:
grep -r "\.find(" --include="*.js" --include="*.ts" | grep -v "service\.find"
grep -r "Model\." --include="*.js" --include="*.ts"
Any call to UserModel.find(), db.collection.find(), or ORM methods directly on the model bypasses Feathers service hooks entirely. Refactor these to app.service('users').find(params).
Fixes Tied to Findings
Fix: Re-register Hooks on Correct Service Instance
Move hook registration after service initialization and ensure path matches exactly:
// app.js (v4/v5 compatible)
const MyService = require('./services/my-service');
app.use('/my-service', new MyService());
// Register hooks AFTER use()
app.service('/my-service').hooks({
before: {
create: [validateInput, setDefaults],
find: [authenticate('jwt')]
},
after: {
create: [logActivity],
all: [populateRelations]
},
error: {
all: [handleError]
}
});
For v5 class-based services, define hooks on the class:
// services/my-service.js (v5)
const { Service } = require('@feathersjs/feathers');
const { hooks } = require('@feathersjs/hooks');
class MyService extends Service {
// ... methods
}
module.exports = MyService;
// In app.js
const MyService = require('./services/my-service');
app.use('/my-service', new MyService());
// Hooks can also be added here if not on class
Fix: Return/Await Context in Async Hooks
Every async hook must return the context (or a modified context). Missing return breaks the chain:
// BROKEN - chain stops here
module.exports = () => async context => {
await doAsyncWork(context.data);
// no return -> subsequent hooks never run
};
// FIXED
module.exports = () => async context => {
await doAsyncWork(context.data);
return context; // or return { ...context, data: newData };
};
If you need to modify the context, return the modified object. Returning undefined or nothing is equivalent to breaking the chain.
Fix: Route All Mutations Through Service Layer
Replace direct model calls with service calls. Example refactor:
// BEFORE - bypasses hooks
const User = require('../models/User');
async function createUser(data) {
return User.create(data); // hooks never fire
}
// AFTER - triggers hooks
const app = require('../app');
async function createUser(data) {
return app.service('users').create(data); // before/after/error hooks run
}
For internal server-to-server calls, obtain the app instance and call app.service('path').method(). This ensures hooks execute even without HTTP transport.
Escalation Criteria
Escalate to deeper debugging when:
- All four checks pass but hooks still do not execute — suspect version mismatch between
@feathersjs/feathersand hook plugins; verifypackage.jsonand consult exact version docs. - Hooks execute but
errorhooks never catch thrown errors — verify error hook registration useserror: { all: [handler] }and that errors are thrown (not returned) in service methods. - Hook order differs from registration order — Feathers sorts hooks by type (
allthen method-specific) and registration sequence; logcontext.typeandcontext.methodto map actual order. - TypeScript compilation strips hook metadata — ensure
emitDecoratorMetadataandexperimentalDecoratorsare enabled if using decorators.
Verification Steps
- Inspect
package.jsonfor@feathersjs/feathersmajor version. Match hook registration syntax to that version's documentation. - Add temporary
console.loginside the hook, call the service via Feathers client, confirm log appears. - Reproduce the call over REST and Socket.io in parallel; compare hook execution.
- After applying fixes, run integration tests covering create/update/patch/remove/find across both transports.
Limitations
This guide covers the most common hook execution failures in Feathers v4 and v5. It does not address custom transport adapters, third-party hook libraries that wrap Feathers hooks, or application-level error handlers that swallow hook errors. If your stack includes those, extend the diagnostic checks accordingly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.