ข้ามไปเนื้อหาหลัก

Category: guide

JavaScript Error Handling — try/catch, Custom Errors, Result Pattern

Patterns สำหรับจัดการ errors ใน JavaScript/TypeScript: custom error classes, error chaining, Result type, async error handling

· อ่านประมาณ 4 นาที

สารบัญ

Error Types ใน JavaScript

// Built-in error types
new Error('message')           // generic
new TypeError('wrong type')    // type mismatch
new RangeError('out of range') // value out of range
new ReferenceError('x is not defined')
new SyntaxError('bad JSON')    // จาก JSON.parse()
new URIError('bad URI')

// Properties ที่ทุก Error มี
const err = new Error('something failed');
err.message  // 'something failed'
err.name     // 'Error'
err.stack    // stack trace string

Custom Error Classes

// ✓ Custom error ที่ instanceof ทำงานถูกต้อง
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode?: number,
  ) {
    super(message);
    this.name = 'AppError';  // สำคัญ: set name เอง
    // Fix for TypeScript targeting ES5/ES2015
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 'NOT_FOUND', 404);
    this.name = 'NotFoundError';
    Object.setPrototypeOf(this, NotFoundError.prototype);
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly fields: Record<string, string>,
  ) {
    super(message, 'VALIDATION_ERROR', 422);
    this.name = 'ValidationError';
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

// ใช้งาน:
throw new NotFoundError('User');
throw new ValidationError('Invalid input', { email: 'required' });

// Check type:
try {
  await getUser(id);
} catch (err) {
  if (err instanceof NotFoundError) {
    return res.status(404).json({ error: err.message });
  }
  if (err instanceof ValidationError) {
    return res.status(422).json({ error: err.message, fields: err.fields });
  }
  throw err; // re-throw ถ้าไม่รู้จัก
}

Error Chaining (Cause)

// ES2022+: ส่ง original error ไปด้วย
try {
  await db.query('SELECT * FROM users');
} catch (dbError) {
  throw new Error('Failed to load users', { cause: dbError });
}

// อ่าน cause:
try {
  await loadUsers();
} catch (err) {
  console.error(err.message);           // 'Failed to load users'
  console.error((err as Error & { cause?: Error }).cause?.message); // db error message
}

Async Error Handling

// ✓ await ใน try/catch
async function fetchUser(id: string) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new AppError(`HTTP ${res.status}`, 'FETCH_ERROR', res.status);
    return await res.json();
  } catch (err) {
    if (err instanceof AppError) throw err;
    throw new AppError('Network error', 'NETWORK_ERROR', undefined);
  }
}

// ❌ ลืม await → catch ไม่ได้
async function bad() {
  try {
    fetchUser('1');  // ไม่มี await → Promise rejection ไม่ถูก catch
  } catch (err) {
    // ไม่มาถึงตรงนี้!
  }
}

// Unhandled rejection handler (global fallback)
process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
  process.exit(1);
});

Result Pattern (functional approach)

แทนที่จะ throw ใช้ return result ที่บอกว่า success หรือ failure:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

// Helper functions
const ok = <T>(value: T): Result<T> => ({ ok: true, value });
const err = <E extends Error>(error: E): Result<never, E> => ({ ok: false, error });

// Function ที่ไม่ throw
async function safeParseJSON(text: string): Promise<Result<unknown, SyntaxError>> {
  try {
    return ok(JSON.parse(text));
  } catch (e) {
    return err(e as SyntaxError);
  }
}

// ใช้งาน — ไม่ต้อง try/catch
const result = await safeParseJSON(rawText);
if (!result.ok) {
  console.error('Parse error:', result.error.message);
  return;
}
const data = result.value;  // TypeScript รู้ว่า type ถูกต้อง

// เปรียบเทียบ:
// throw-based: ต้อง wrap ทุก call ด้วย try/catch
// Result-based: explicit handling, compiler บังคับ check

Error Boundaries (React-style, ไม่ใช่ React)

// Generic wrapper สำหรับ sync functions
function tryCatch<T, E extends Error>(
  fn: () => T,
): Result<T, E> {
  try {
    return { ok: true, value: fn() };
  } catch (e) {
  return { ok: false, error: e as E };
  }
}

// Generic wrapper สำหรับ async
async function tryCatchAsync<T, E extends Error>(
  fn: () => Promise<T>,
): Promise<Result<T, E>> {
  try {
    return { ok: true, value: await fn() };
  } catch (e) {
    return { ok: false, error: e as E };
  }
}

// ใช้งาน:
const result = await tryCatchAsync(() => fetchUser(id));
if (!result.ok) handleError(result.error);
else useData(result.value);

Rethrowing และ Error Filtering

// ✓ pattern ที่ถูกต้อง: handle เฉพาะที่รู้จัก, rethrow อื่น
async function processFile(path: string) {
  try {
    return await fs.readFile(path, 'utf8');
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
      return null;  // ไม่มีไฟล์ → return null แทน throw
    }
    throw err;  // อื่นๆ rethrow ให้ caller จัดการ
  }
}

// ❌ swallow all errors — อย่าทำ
try {
  doSomething();
} catch (err) {
  // ไม่ทำอะไรเลย — bug จะหายไป
}

// ❌ catch แล้ว throw Error ใหม่โดยไม่บอก cause
try {
  await connect();
} catch (err) {
  throw new Error('Connection failed'); // เสีย stack trace ของ original
}

// ✓ ส่ง cause ไปด้วย
throw new Error('Connection failed', { cause: err });

Zod Validation Errors

import { z } from 'zod';

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().min(0).max(120),
});

// Safe parse — ไม่ throw
const result = UserSchema.safeParse(input);
if (!result.success) {
  const errors = result.error.flatten().fieldErrors;
  // errors = { name: ['Required'], email: ['Invalid email'] }
  throw new ValidationError('Invalid user data', errors as Record<string, string>);
}
const user = result.data;  // fully typed

// หรือ throw ทันที
const user = UserSchema.parse(input);  // throws ZodError ถ้า invalid

Global Error Logging

// Browser
window.addEventListener('error', (event) => {
  logError({
    message: event.message,
    filename: event.filename,
    line: event.lineno,
  });
});

window.addEventListener('unhandledrejection', (event) => {
  logError({ message: String(event.reason) });
  event.preventDefault(); // suppress default console.error
});

// Node.js
process.on('uncaughtException', (err) => {
  logError(err);
  process.exit(1);  // ต้อง exit — process state อาจเสียหาย
});

process.on('unhandledRejection', (reason) => {
  logError(new Error(String(reason)));
  // ไม่ต้อง exit เสมอไป ขึ้นอยู่กับ context
});