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
สารบัญ
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
});