Error Handling & Response Patterns¶
The API uses a standardized JSON envelope for most responses, but the discipline is inconsistent — handlers freely mix the envelope with raw status codes, plain strings, and bare res.send() calls. This document captures what the conventions are, where they break down, and what to follow when adding new endpoints.
Standard Response Envelope¶
Defined in helper/helper.js:
const getSuccessResponse = (response) => ({
status: true,
errorMsg: "",
response,
});
const getErrorResponse = (errorMsg = "", response = {}) => ({
status: false,
errorMsg,
response,
});
| Field | Type | Notes |
|---|---|---|
status |
boolean | true on success, false on failure |
errorMsg |
string | Empty on success; human-readable message on failure |
response |
any | Payload on success; usually {} on failure |
Both helpers are exported from helper/helper.js and re-exported via helper/index.js. Use them.
Canonical usage¶
const { getSuccessResponse, getErrorResponse } = require('../helper');
router.get('/something', auth, async (req, res) => {
try {
const data = await loadThing(req.userId);
res.send(getSuccessResponse(data));
} catch (err) {
console.error('loadThing failed:', err);
res.send(getErrorResponse(err.message));
}
});
Note that HTTP status code is left at the default 200 even on failure. The client distinguishes success from failure by inspecting response.status (the boolean), not the HTTP code.
Where the Pattern Breaks¶
The codebase is not consistent. You will see all of the following in production code:
1. Direct res.status() with raw JSON¶
// routes/partnerAuth.js
res.status(409).json({ error: 'Already exists' });
// routes/social.js
res.status(401).json({ message: 'Unauthorized' });
These bypass the envelope entirely. The client has to handle two response shapes from the same API surface.
2. Plain-string error bodies¶
// middlewares/auth.js
return res.status(401).send('unauthorized access');
// methods.js
return res.sendStatus(403);
Used in middleware paths.
3. Bare res.send() without status¶
Defaults to HTTP 200 even when the operation failed in some logical sense.
Practical consequence¶
The frontend must defensively check both response.status === false (envelope) and HTTP status >= 400 (raw) to detect failure. When adding new endpoints, follow the envelope pattern unless you have a reason not to.
Status Code Conventions (in routes that use them)¶
When handlers do set explicit status codes, this is the de-facto mapping:
| Code | Used for |
|---|---|
200 |
Success (default; rarely set explicitly) |
400 |
Bad request — validation failure, missing fields |
401 |
Authentication failure — token missing / invalid |
403 |
Forbidden — token decryption failed (alternative auth path in methods.js) |
404 |
Resource not found |
409 |
Conflict — duplicate registration (e.g., routes/partnerAuth.js) |
500 |
Unhandled server error |
There is no centralized mapping — these are the patterns observed in routes/social.js, routes/partnerAuth.js, and routes/paddle.js. Other route files mostly stay at 200 with the envelope.
CRUD Layer — actions/actions.js¶
The actions class wraps generic CRUD operations. Errors are not thrown — they are passed back via callback as null:
apiAction.prototype.getAll = function (table, where, cb) {
this.db.query(sql, params, (err, result) => {
if (err) {
console.error('getAll error:', err);
cb(null); // <- null signals failure
return;
}
cb(result);
});
};
All CRUD methods (getAll, insertData, updateData, removeData, customQuery, fetchAll) follow this pattern.
Implications for callers¶
action.getAll('tUsers', { id: req.userId }, (rows) => {
if (!rows) {
return res.send(getErrorResponse('Database error'));
}
if (rows.length === 0) {
return res.send(getErrorResponse('User not found'));
}
res.send(getSuccessResponse(rows[0]));
});
You must check for null explicitly. Empty arrays mean "no rows," not "error." A null result has lost the original error message — the only way to find out what went wrong is to check the server logs (console.error).
Middleware Errors¶
middlewares/auth.js¶
Returns plain string "unauthorized access" with HTTP 401 on:
- Missing token header
- Token decryption failure
- Token JWT verification failure
methods.js (alternative auth path)¶
Returns HTTP 403 with no body on token decryption failure:
These middleware error shapes do not match the envelope. The frontend treats 401/403 as a special case and short-circuits to a re-auth flow.
No Global Error Handler¶
There is no Express error-handling middleware registered:
Consequences:
- Uncaught exceptions in async handlers propagate to the default Express handler (HTML error page in dev, generic 500 in prod).
- Synchronous throw inside a handler that's not in a try/catch will bubble up the same way.
- There is no central place to log unhandled errors, transform them into the envelope, or strip stack traces from prod responses.
Implication¶
Every async handler must wrap its own logic in try/catch. If you don't, a single unhandled rejection in an async function will leak a 500 with no logging, no envelope, and possibly a stack trace to the client (depending on Express defaults).
Recommended Pattern for New Endpoints¶
const { getSuccessResponse, getErrorResponse } = require('../helper');
router.post('/some/resource', auth, async (req, res) => {
try {
// 1. Validate inputs (see middlewares/validation.js for the existing approach)
if (!req.body.name) {
return res.status(400).send(getErrorResponse('name is required'));
}
// 2. Do work
const result = await action.insertDataAsync('tThing', { ... });
// 3. Return success
res.send(getSuccessResponse(result));
} catch (err) {
// 4. Log + respond with envelope
console.error('POST /some/resource failed:', err);
res.status(500).send(getErrorResponse('Internal error'));
}
});
Don't:
- ❌ Throw a string — wrap in Error().
- ❌ Send error details (stack traces, SQL errors) directly to the client. Log them server-side and return a generic message.
- ❌ Use res.send(getErrorResponse(err)) with an Error object — errorMsg becomes "[object Object]". Use err.message.
Logging on Error¶
Until structured logging is introduced (see Logging & Observability), the convention is:
The <context> should at minimum identify the function and one or more identifying inputs (account ID, member ID) so a grep-through pm2 logs is tractable.
Open Issues¶
The current state has known sharp edges:
- Inconsistent response shapes — frontend has to handle both envelope and raw JSON.
- No global error handler — unhandled exceptions bypass the envelope and leak.
- Errors lost in CRUD layer —
cb(null)discards the original error. - Middleware returns plain strings — 401/403 don't fit the envelope.
- HTTP 200 on logical failure — clients must inspect the body, not just the status.
When you have the opportunity to clean this up, consider:
- A wrapper for async handlers that catches errors and emits the envelope.
- Migrating actions/actions.js to throw or return a {data, error} shape.
- A global error middleware as a backstop.
Related¶
- Authentication & Security — middleware error shapes
- Logging & Observability — error logging conventions
- Architecture Overview — full middleware stack