{"record":{"id":"fe99a59edc53ed99","repo":"hcengineering/platform","slug":"err-code-fe99a5","errorCode":"err.code","errorMessage":"err.message (500 fallback sends err object when message is empty)","messagePattern":"err\\.message \\(500 fallback sends err object when message is empty\\)","errorType":"http","errorClass":"ApiError","httpStatus":500,"severity":"warning","filePath":"services/analytics-collector/pod-analytics-collector/src/server.ts","lineNumber":289,"sourceCode":"        if (evt.event === AnalyticEventType.Error) {\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          const { error_message, error_type, error_stack } = evt.properties ?? {}\n          reportOTELError({ message: error_message ?? 'Unknown error', stack: error_stack, name: error_type ?? '' })\n        } else {\n          reportOTEL('info', evt.event, evt.timestamp, { ...evt.properties, distinct_id: evt.distinct_id })\n        }\n      }\n    })\n  )\n\n  app.use((err: any, _req: any, res: any, _next: any) => {\n    console.log(err)\n    if (err instanceof ApiError) {\n      res.status(err.code).send({ code: err.code, message: err.message })\n      return\n    }\n\n    res.status(500).send(err.message?.length > 0 ? { message: err.message } : err)\n  })\n\n  return app\n}\n\nasync function sendEventsToPosthog (events: AnalyticEvent[], req: Request): Promise<void> {\n  const posthogEvents: Record<string, any>[] = []\n  for (const evt of events) {\n    posthogEvents.push(await preparePostHogEvent(evt, req))\n  }\n\n  const payload = {\n    api_key: config.PostHogAPI,\n    batch: posthogEvents.reverse()\n  }\n\n  const posthogPayloadSize = JSON.stringify(payload).length\n  console.log(`Sending to PostHog: ${posthogEvents.length} events, ${posthogPayloadSize} bytes`)","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/analytics-collector/pod-analytics-collector/src/server.ts#L271-L307","documentation":"The analytics-collector server's global error handler behaves identically to the ai-bot one: ApiError instances are sent with their code/message, everything else gets a 500 with {message: err.message} or the raw error object when the message is empty. A response containing the whole error object indicates a non-ApiError with an empty message escaped from a route handler.","triggerScenarios":"Unhandled exceptions in analytics ingestion routes that are not ApiError and carry an empty/undefined message — failed event validation libraries throwing bare errors, message-less rejections from sendEventsToPosthog, or thrown non-Error values.","commonSituations":"Posthog/network calls failing with opaque errors; schema validation throwing message-less errors; bugs throwing plain objects; missing await causing rejected promises to hit the error middleware.","solutions":["Inspect the raw error object returned in the 500 response to find the root cause.","Convert expected failures (validation, upstream errors) into ApiError with a proper code and message.","Ensure all async handlers await promises so rejections carry messages.","Add logging/middleware to normalize thrown non-Error values into Errors."],"exampleFix":"// before\nsendEventsToPosthog(events, req) // floating promise\n// after\ntry {\n  await sendEventsToPosthog(events, req)\n} catch (e) {\n  throw new ApiError(502, `posthog delivery failed: ${String(e)}`)\n}","handlingStrategy":"try-catch","validationCode":"function extractServerMessage(body: unknown): string {\n  if (typeof body === 'object' && body !== null && 'message' in body && typeof (body as any).message === 'string') return (body as any).message\n  return 'unstructured analytics-collector error: ' + JSON.stringify(body)\n}","typeGuard":"function isApiErrorPayload(x: unknown): x is { code: number; message: string } {\n  return typeof x === 'object' && x !== null && typeof (x as any).code === 'number' && typeof (x as any).message === 'string'\n}","tryCatchPattern":"app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {\n  if (err instanceof ApiError) {\n    return res.status(err.code).json({ code: err.code, message: err.message })\n  }\n  console.log(err)\n  const message = err instanceof Error && err.message.length > 0 ? err.message : 'internal error'\n  return res.status(500).json({ message })\n})","preventionTips":["Wrap Posthog/upstream delivery failures in ApiError with descriptive messages.","Validate event payloads before ingestion and throw typed errors.","Never throw non-Error values; log-and-convert unknown throws centrally.","Add tests covering the 500 fallback to ensure responses stay structured."],"tags":["http-500","error-handling","express"],"backgroundTag":"unhandled-server-error","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}