Skip to content

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

// routes/routes.js (multiple handlers)
res.send({ data: result });

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

if (!authHeader) return res.status(401).send('unauthorized access');

methods.js (alternative auth path)

Returns HTTP 403 with no body on token decryption failure:

if (!verified) return res.sendStatus(403);

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:

// This is NOT present in server.js:
app.use((err, req, res, next) => { ... });

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).


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:

console.error('<context>:', err);

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:

  1. Inconsistent response shapes — frontend has to handle both envelope and raw JSON.
  2. No global error handler — unhandled exceptions bypass the envelope and leak.
  3. Errors lost in CRUD layercb(null) discards the original error.
  4. Middleware returns plain strings — 401/403 don't fit the envelope.
  5. 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.