Skip to content

Control Flow & Organization

Guidelines for control flow, logging, error handling, and code organization.

Control Flow Patterns

Mandatory Braces

Always use braces for if, for, while, and do-while statements, even for single-line bodies. Never use inline statements.

typescript
// ✅ GOOD - Always use braces
if (condition) {
  doSomething();
}

for (const item of items) {
  process(item);
}

while (running) {
  tick();
}

do {
  attempt();
} while (retrying);

// ❌ BAD - Never inline without braces
if (condition) doSomething();
for (const item of items) process(item);
while (running) tick();

Why braces are mandatory:

  • Prevents bugs when adding statements later
  • Clearer code structure at a glance
  • Consistent formatting across codebase

Switch Statement Requirements

All switch statements must:

  1. Use braces {} for each case block
  2. Include a default case (even if it throws)
typescript
// ✅ GOOD - Braces and default case
switch (status) {
  case 'active': {
    activateUser();
    break;
  }
  case 'inactive': {
    deactivateUser();
    break;
  }
  case 'pending': {
    notifyAdmin();
    break;
  }
  default: {
    throw getError({
      statusCode: HTTP.ResultCodes.RS_4.BadRequest,
      message: `Unknown status: ${status}`,
    });
  }
}

// ❌ BAD - Missing braces and default case
switch (status) {
  case 'active':
    activateUser();
    break;
  case 'inactive':
    deactivateUser();
    break;
  // Missing default case!
}

Why these rules:

  • Braces prevent variable scoping issues between cases
  • Default case ensures all values are handled
  • Throwing in default catches unexpected values early

Logging Patterns

Method Context Prefix

Always include class and method context in log messages:

typescript
// Format: [ClassName][methodName] Message with %s placeholders
this.logger.info('[binding] Asset storage bound | Key: %s | Type: %s', key, storageType);
this.logger.debug('[authenticate] Token validated | User: %s', userId);
this.logger.warn('[register] Skipping duplicate registration | Type: %s', opts.type);
this.logger.error('[generate] Token generation failed | Error: %s', error.message);

Structured Data

Use format specifiers for structured logging:

typescript
// %s - string, %d - number, %j - JSON object
this.logger.info('[create] User created | ID: %s | Email: %s', user.id, user.email);
this.logger.debug('[config] Server options: %j', this.serverOptions);

NOTE

%j on an Error instance drops message and stack - they are non-enumerable, so JSON.stringify never sees them. Use %s for errors, %j/%o for plain data objects. An object passed to %s is inspected up to APP_ENV_LOGGER_INSPECT_DEPTH levels deep (default 5) instead of Node's hard-coded depth: 0, so nested fields print instead of collapsing to [Object]. See Logger Helper for details.

Log Levels

Exactly five levels exist, each a direct method on ILogger:

LevelUse For
emergThe process cannot continue
errorExceptions that need attention
warnRecoverable issues, deprecations
infoImportant business events
debugDetailed debugging information

NOTE

Secret-looking keys (token, password, apiKey, ...) are redacted to [REDACTED] before a line reaches any transport. APP_ENV_LOGGER_DO_REDACT=false disables it - local debugging only.

Never Swallow a Catch

An empty or comment-only catch is banned. Log, then rethrow or return a defined fallback.

typescript
// ✅ GOOD
try {
  await syncRemote();
} catch (error) {
  this.logger.error('[syncRemote] Sync failed | Error: %s', error);
  throw error;
}

// ❌ BAD - the failure disappears
try {
  await syncRemote();
} catch {
  // ignore
}

Standardized Error Handling

Use the getError helper and HTTP constants to throw consistent, formatted exceptions. Never new Error. Across package boundaries test identity with isApplicationError(), never instanceof - the class has more than one identity in a monorepo.

The normalized message is always one shape: { text, code, args }, read from error.normalized.

Basic Error

typescript
import { getError, HTTP } from '@venizia/ignis-helpers';

if (!record) {
  throw getError({
    statusCode: HTTP.ResultCodes.RS_4.NotFound,
    message: 'Record not found',
    details: { id: requestedId },
  });
}

Error with Context

Include class/method context in error messages:

typescript
// Format: [ClassName][methodName] Descriptive message
throw getError({
  statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
  message: '[JWSTokenService][generate] Failed to generate token',
});

throw getError({
  statusCode: HTTP.ResultCodes.RS_4.Unauthorized,
  message: '[AuthMiddleware][authenticate] Missing authorization header',
});

Validation Errors

typescript
constructor(options: IServiceOptions) {
  if (!options.apiKey) {
    throw getError({
      statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
      message: '[PaymentService] Missing required apiKey configuration',
    });
  }
}

HTTP Status Code Quick Reference

CategoryConstantUse Case
SuccessHTTP.ResultCodes.RS_2.OkSuccessful response
CreatedHTTP.ResultCodes.RS_2.CreatedResource created
Bad RequestHTTP.ResultCodes.RS_4.BadRequestInvalid input
UnauthorizedHTTP.ResultCodes.RS_4.UnauthorizedMissing/invalid auth
ForbiddenHTTP.ResultCodes.RS_4.ForbiddenInsufficient permissions
Not FoundHTTP.ResultCodes.RS_4.NotFoundResource not found
Internal ErrorHTTP.ResultCodes.RS_5.InternalServerErrorServer errors

Code Organization

Section Separator Comments

Use visual separators for major code sections in long files:

typescript
// ---------------------------------------------------------------------------
// Type Definitions
// ---------------------------------------------------------------------------

type TMyType = { /* ... */ };

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const DEFAULT_OPTIONS = { /* ... */ };

// ---------------------------------------------------------------------------
// Main Implementation
// ---------------------------------------------------------------------------

export class MyClass {
  // ...
}

Guidelines:

  • Use for files > 200 lines with distinct sections
  • Use 75-character wide separator lines
  • Descriptive section names (2-4 words)

Import Organization Order

Organize imports in this order:

typescript
// 1. Node built-ins (with 'node:' prefix)
import fs from 'node:fs';
import path from 'node:path';

// 2. Third-party packages (alphabetical)
import { z } from '@hono/zod-openapi';
import dayjs from 'dayjs';

// 3. Internal absolute imports (by domain/package)
import { getError } from '@venizia/ignis-helpers';
import { AbstractEntity } from '@/base/models';
import { UserService } from '@/services';

// 4. Relative imports (same feature) - LAST
import { AbstractRepository } from './base';
import { QueryBuilder } from '../query';

Rules:

  • Blank line between each group
  • Alphabetical within each group
  • node: prefix for Node.js built-ins
  • Relative imports only for same feature/module

See Also