Skip to content

Error handling

The reason this library exists. When a render fails you get a typed error with a structured details payload — the missing variable, the source location, the context you passed, and how to fix it.

The shape of a render error

typescript
import { TemplateRenderError, isTemplateError } from '@ackplus/nest-dynamic-templates';

try {
  await templates.render({ name: 'welcome-email', context: { email: 'a@b.com' } });
} catch (err) {
  if (isTemplateError(err)) {
    err.code;                    // 'TEMPLATE_RENDER_FAILED'
    err.getStatus();             // 422
    err.details.missingVariable; // 'firstName'
    err.details.contextKeys;     // ['email']  ← what you actually passed
    err.details.location;        // { line: 1, column: 16 }
    err.details.snippet;         // '<h1>Hello {{ firstName }}</h1>'
    err.details.stage;           // 'template-engine'
    err.details.engine;          // 'njk'
    err.details.hint;            // 'Pass "firstName" in the render context. ...'
    err.cause;                   // the original engine error
  }
}

The message reads:

Failed to render template "welcome-email" [njk → html, scope=system, locale=en]: variable "firstName" is undefined.

details fields

FieldDescription
codeStable TemplateErrorCode.
stagesubject · template-engine · language-engine · layout.
engine / languageThe processors in use.
templateName, scope, scopeId, localeWhich template was being rendered.
missingVariableThe undefined variable, when detectable.
contextKeysThe keys present in your render context.
location{ line, column } in the template source.
snippetThe offending source line.
hintA concrete next step.

Errors map to HTTP statuses

Each error extends its matching NestJS exception, so Nest's exception filter sets the status automatically and existing catch blocks keep working.

ErrorHTTPcodeExtends
TemplateRenderError422TEMPLATE_RENDER_FAILEDUnprocessableEntityException
TemplateNotFoundError404TEMPLATE_NOT_FOUNDNotFoundException
TemplateInputError400TEMPLATE_INVALID_INPUTBadRequestException
TemplateForbiddenError403TEMPLATE_FORBIDDENForbiddenException
TemplateConflictError409TEMPLATE_CONFLICTConflictException
TemplateEngineUnavailableError500TEMPLATE_ENGINE_UNAVAILABLEInternalServerErrorException

Handling them

Switch on code

typescript
import { isTemplateError, TemplateErrorCode } from '@ackplus/nest-dynamic-templates';

catch (err) {
  if (!isTemplateError(err)) throw err;
  switch (err.code) {
    case TemplateErrorCode.NOT_FOUND:
      return fallbackTemplate();
    case TemplateErrorCode.RENDER_FAILED:
      logger.error({ ...err.details }); // structured logging
      throw err;
    default:
      throw err;
  }
}

Let NestJS handle the response

Because the errors are HTTP exceptions, you can simply let them bubble out of a controller — the client gets a clean JSON body:

json
{
  "statusCode": 422,
  "error": "TemplateRenderError",
  "code": "TEMPLATE_RENDER_FAILED",
  "message": "Failed to render template \"welcome-email\" [njk → html, scope=system, locale=en]: variable \"firstName\" is undefined.",
  "stage": "template-engine",
  "missingVariable": "firstName",
  "contextKeys": ["email"],
  "location": { "line": 1, "column": 16 },
  "snippet": "<h1>Hello {{ firstName }}</h1>",
  "hint": "Pass \"firstName\" in the render context. The render context has: [email]. To make it optional, guard it in the template, e.g. {{ firstName or \"\" }}."
}

Want missing variables to render empty instead of throwing?

Nunjucks throws on undefined by default (throwOnUndefined: true). To make variables optional, either guard them in the template ({{ firstName or "" }}) or pass engineOptions.template.njk = { throwOnUndefined: false }.

Released under the MIT License.