Home » blog » 10 JavaScript Error Handling Patterns That Actually Matter

10 JavaScript Error Handling Patterns That Actually Matter

কোড যখন ছোট থাকে, তখন এরর (error) হ্যান্ডেল করা বেশ সহজ। একটা ফাংশন কল করলেন, সেটাকে try/catch এর ভেতর রাখলেন, এরর লগ করলেন, আর কাজ শেষ।

কিন্তু রিয়েল-লাইফ অ্যাপ্লিকেশন সম্পূর্ণ আলাদা। এখানে প্রমিসের ভেতরে, টাইমার, ইভেন্ট হ্যান্ডেলার, এপিআই (API) কল, রেন্ডারিং কোড অথবা যে ফাংশনটি অপারেশন শুরু করেছিল তার কয়েক লেয়ার নিচেও এরর দেখা দিতে পারে। প্রপার JavaScript error handling ছাড়া কোনো অ্যাপ্লিকেশন প্রোডাকশনে নেওয়া বেশ রিস্কি।

10 JavaScript Error Handling Patterns
10 JavaScript Error Handling Patterns

সবচেয়ে কঠিন কাজটা catch লেখা নয়। বরং কঠিন হলো একটি এরর কোথায় হওয়া উচিত, কে সেটা হ্যান্ডেল করবে এবং এরপর কী ঘটবে, তা নির্ধারণ করা।

আজ আমরা এমন 10 JavaScript error handling প্যাটার্ন নিয়ে আলোচনা করব, যা প্রতিদিনের জাভাস্ক্রিপ্ট ডেভেলপমেন্টে আপনার কাজে আসবে এবং আপনার কোডকে আরও রোবাস্ট করবে।

1. Keep async error handling in one place

প্রমিস চেইনের নিজস্ব JavaScript error handling ব্যবস্থা আছে:

JavaScript

fetchUser(userId)
  .then((user) => {
    renderUser(user);
  })
  .catch((error) => {
    console.error("Failed to load user:", error);
  });

এই অ্যাপ্রোচে কোনো ভুল নেই। কিন্তু যখন একটা ফাংশন ইতিমধ্যেই async/await ব্যবহার করছে, তখন try/catch ব্লক ব্যবহার করলে কোডের ফ্লো বোঝা অনেক সহজ হয়ে যায়।

JavaScript

async function loadUser(userId) {
  try {
    const user = await fetchUser(userId);
    renderUser(user);
  } catch (error) {
    console.error("Failed to load user:", error);
  }
}

এটি সবচেয়ে বেশি কাজে আসে যখন একাধিক অপারেশন একই টাস্কের অংশ হয়:

JavaScript

async function loadDashboard(userId) {
  try {
    const user = await fetchUser(userId);
    const projects = await fetchProjects(user.id);

    renderDashboard({ user, projects });
  } catch (error) {
    console.error("Failed to load dashboard:", error);
  }
}

অ্যাপ্লিকেশনের দৃষ্টিকোণ থেকে এখন এই পুরো ব্লকটি একটিমাত্র অপারেশনকে রিপ্রেজেন্ট করছে।

এখানে গুরুত্বপূর্ণ বিষয়টা হলো, রিজেক্টেড (rejected) প্রমিসের জন্য সবসময় একজন কনজিউমার বা হ্যান্ডেলার থাকতে হবে।

যদি কোনো async ফাংশন রিজেক্ট হয় এবং কেউ যদি সেটাকে await বা .catch() দিয়ে হ্যান্ডেল না করে, তবে সেই রিজেকশনটি unhandled থেকে যায়।

তাই নিচের কোডটি রিস্কি:

JavaScript

loadDashboard(userId);

যদি কলার (caller) এই ফেইলিওরের জন্য দায়ী হয়, তবে তাকে সেখানেই এটি হ্যান্ডেল করতে হবে:

JavaScript

loadDashboard(userId).catch((error) => {
  reportError(error);
});

সহজ একটা নিয়ম মনে রাখবেন: প্রতিটি প্রমিসের ফেইলিওর হ্যান্ডেল করার জন্য শেষ পর্যন্ত একজন নির্দিষ্ট ওনার (owner) থাকতে হবে।

2. try/catch does not follow asynchronous callbacks

প্রথমে এই কোডটি দেখতে বেশ যুক্তিসঙ্গত মনে হতে পারে:

JavaScript

try {
  setTimeout(() => {
    throw new Error("Something went wrong");
  }, 100);
} catch (error) {
  console.error("Caught:", error);
}

কিন্তু catch ব্লক কখনোই সেই এররটি রিসিভ করে না। কারণ, অরিজিনাল try ব্লকটি শেষ হয়ে যাওয়ার অনেক পরে কলব্যাক ফাংশনটি রান করে।

যদি কলব্যাক নিজেই কোনো এরর থ্রো করতে পারে, তবে সেই এক্সিকিউশন পাথের ভেতরেই এররটি হ্যান্ডেল করুন:

JavaScript

setTimeout(() => {
  try {
    runBackgroundTask();
  } catch (error) {
    console.error("Background task failed:", error);
  }
}, 100);

একই ধারণা অন্যান্য কলব্যাক-বেসড API-গুলোর ক্ষেত্রেও প্রযোজ্য:

JavaScript

button.addEventListener("click", () => {
  try {
    updateAccount();
  } catch (error) {
    showErrorMessage(error);
  }
});

প্রমিসগুলো একটু ভিন্নভাবে কাজ করে, কারণ রিজেকশন প্রমিস চেইনের মধ্য দিয়ে ট্রাভেল করে:

JavaScript

Promise.resolve()
  .then(() => {
    throw new Error("Failed");
  })
  .catch((error) => {
    console.error("Caught:", error);
  });

অ্যাসিনক্রোনাস কোড নিয়ে কাজ করার সময় সবসময় নিজেকে একটি প্রশ্ন করবেন: এই কলব্যাক বা প্রমিস থেকে আসা এররটি কোথায় যাবে? যদি এর কোনো ক্লিয়ার উত্তর না থাকে, তার মানে আপনার কোডে একটি মিসিং এরর পাথ রয়েছে।

3. Add a global fallback for unhandled promise rejections

লোকাল JavaScript error handling এর মাধ্যমেই আপনার বেশিরভাগ কাজ হওয়া উচিত। তবুও, যেসব এরর সাধারণ হ্যান্ডেলারদের চোখ এড়িয়ে যায়, তাদের জন্য একটি গ্লোবাল সেফটি নেট বা ফলব্যাক থাকা অ্যাপ্লিকেশনের জন্য বেশ উপকারী।

ব্রাউজারের ক্ষেত্রে:

JavaScript

window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled promise rejection:", event.reason);
  reportError(event.reason);
});

আপনি চাইলে আনকট (uncaught) সিনক্রোনাস এররগুলোকেও ধরতে পারেন:

JavaScript

window.addEventListener("error", (event) => {
  reportError(event.error ?? event.message);
});

Node.js এর ক্ষেত্রে:

JavaScript

process.on("unhandledRejection", (reason) => {
  console.error("Unhandled promise rejection:", reason);
  reportError(reason);
});

তবে এই হ্যান্ডেলারগুলোকে আপনার অ্যাপ্লিকেশনের মূল আর্কিটেকচার হিসেবে ব্যবহার করবেন না। এদের উদ্দেশ্য হলো যে ফেইলিওরগুলো সবকিছু এড়িয়ে গেছে, সেগুলো ধরা এবং আপনাকে জানানো।

প্রোডাকশনে শুধুমাত্র কনসোলে লগ করাটাই যথেষ্ট নয়।

অ্যাপ্লিকেশন যখন অন্য কারও সার্ভার বা মেশিনে চলবে, তখন ঠিক কী ঘটেছিল তা তদন্ত করার জন্য আপনার পর্যাপ্ত কনটেক্সট প্রয়োজন। এর মধ্যে স্ট্যাক ট্রেস, অ্যাপ্লিকেশনের ভার্সন, ইউজার কী করছিল, রিকোয়েস্ট আইডি ইত্যাদি থাকতে পারে।

গ্লোবাল হ্যান্ডেলারগুলোকে ফার্স্ট লাইন অফ ডিফেন্স না ভেবে, বরং শেষ অ্যালার্ম হিসেবে বিবেচনা করুন।

4. Create error classes for failures you need to distinguish

সব জায়গায় শুধু Error থ্রো করা ততক্ষণ পর্যন্ত ঠিক আছে, যতক্ষণ না কলারদের বিভিন্ন ধরণের ফেইলিওরের জন্য আলাদাভাবে রিয়্যাক্ট করার প্রয়োজন হয়। যেমন ভ্যালিডেশন এবং নেটওয়ার্ক ফেইলিওর।

JavaScript

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

class HttpError extends Error {
  constructor(message, status) {
    super(message);
    this.name = "HttpError";
    this.status = status;
  }
}

এখন একটা ফাংশন শুধুমাত্র টেক্সট মেসেজের চেয়েও বেশি কিছু রিটার্ন করতে পারে:

JavaScript

function validateUser(user) {
  if (!user.name?.trim()) {
    throw new ValidationError("Name is required", "name");
  }
}

যার ফলে কলার বুঝতে পারে আসলে ঠিক কোন ধরনের এরর হয়েছে:

JavaScript

try {
  validateUser(formData);
  await saveUser(formData);
} catch (error) {
  if (error instanceof ValidationError) {
    showFieldError(error.field, error.message);
    return;
  }

  throw error;
}

লক্ষ্য করুন, একদম শেষে throw error; লেখা হয়েছে। এটা খুবই গুরুত্বপূর্ণ। বর্তমান লেয়ারটি হয়তো জানে কীভাবে ভ্যালিডেশন এরর হ্যান্ডেল করতে হয়, কিন্তু ডাটাবেস বা নেটওয়ার্ক ফেইলিওর নিয়ে তার কোনো আইডিয়া নেই। তাই তার উচিত নয় সাইলেন্টলি এররটাকে ইগনোর করা। কাস্টম এরর ক্লাস তখনই সবচেয়ে বেশি কাজে দেয়, যখন তারা এমন কোনো ইনফরমেশন বহন করে যার ওপর ভিত্তি করে কলার কোনো একশন নিতে পারে।

5. Preserve the original error with cause

অনেক সময় স্ট্যাকের উপরের দিকে যাওয়ার আগে একটি লো-লেভেল এররের সাথে আরও কিছু কনটেক্সট বা প্রেক্ষাপট যুক্ত করার প্রয়োজন হয়। ধরুন, একটি API রিকোয়েস্ট ফেইল করেছে:

JavaScript

async function fetchProfile(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    throw new Error("Could not load user profile", {
      cause: error,
    });
  }
}

হাই-লেভেল মেসেজটি বোঝাচ্ছে অ্যাপ্লিকেশন আসলে কী করার চেষ্টা করছিল: Could not load user profile। কিন্তু অরিজিনাল এররটিও error.cause এর মাধ্যমে সংরক্ষিত থাকছে।

অরিজিনাল ফেইলিওরকে সম্পূর্ণ মুছে দিয়ে নতুন একটি এরর থ্রো করার চেয়ে এটি অনেক ভালো উপায়। cause ব্যবহার না করলে নিচের মতো কোডে অনেক দরকারী কনটেক্সট হারিয়ে যায়:

JavaScript

catch {
  throw new Error("Request failed");
}

একটি ভালো এরর মেসেজে সাধারণত দুটি তথ্যের প্রয়োজন হয়: ১. কোন অপারেশনটি ফেইল করেছে? ২. কেন ফেইল করেছে?

Error.cause আপনাকে এই দুটি তথ্যই সুন্দরভাবে সংরক্ষণ করার সুযোগ দেয়।

6. Decide when failure is exceptional and when it is normal

প্রতিটি ব্যর্থ অপারেশন বা ফেইলিওর এর জন্যই যে এক্সেপশন থ্রো করতে হবে, এমন কোনো কথা নেই। ধরুন, কোনো কিছু সার্চ করলে রেজাল্ট হিসেবে কিছুই না-ও আসতে পারে। আপনি হয়তো এভাবে কোড লিখতে পারেন:

JavaScript

function findUser(id) {
  const user = users.find((user) => user.id === id);

  if (!user) {
    throw new Error("User not found");
  }

  return user;
}

কিন্তু যদি “not found” একটি প্রত্যাশিত বা স্বাভাবিক ফলাফল হয়, তবে null রিটার্ন করলে তা API-কে আরও স্পষ্টভাবে বর্ণনা করে:

JavaScript

function findUser(id) {
  return users.find((user) => user.id === id) ?? null;
}

তখন কলার সেই অনুযায়ী স্বাভাবিকভাবেই ডিসিশন নিতে পারে:

JavaScript

const user = findUser(userId);

if (!user) {
  showEmptyState();
  return;
}

showUser(user);

এর সাথে এমন একটি ফাংশনের তুলনা করুন যেখানে ফেইলিওরের কারণে সত্যিই অপারেশনটি বন্ধ করে দিতে হয়:

JavaScript

async function chargeOrder(order) {
  const response = await paymentClient.charge(order);

  if (!response.ok) {
    throw new PaymentError(
      "Payment could not be completed",
      response.status
    );
  }

  return response.receipt;
}

বিজনেস ফেইলিওর মানেই ভ্যালু রিটার্ন করতে হবে এবং প্রোগ্রামিং ফেইলিওর মানেই এরর থ্রো করতে হবে এমন কোনো ইউনিভার্সাল নিয়ম নেই।

বরং নিজেকে প্রশ্ন করুন: এই ফলাফল কি ফাংশনটির প্রত্যাশিত কন্ট্রাক্টের অংশ, নাকি এর মানে হলো পুরো অপারেশনটিই ফেইল করেছে?

7. Never catch an error just to make it disappear

এটি ডেভেলপারদের করা সবচেয়ে সাধারণ এবং ক্ষতিকর বাগগুলোর একটি:

JavaScript

try {
  await saveSettings(settings);
} catch (error) {
  // Ignore
}

এতে অ্যাপ্লিকেশন এমনভাবে চলতে থাকে যেন সেভ করার কাজটি সফল হয়েছে।

পরে কেউ হয়তো এসে রিপোর্ট করবে যে তাদের সেটিংস বারবার মুছে যাচ্ছে বা সেভ হচ্ছে না। এখন আপনার দুটি সমস্যা: প্রথমত, অরিজিনাল ফেইলিওর; দ্বিতীয়ত, কেন এমন হচ্ছে তার কোনো প্রমাণ বা লগ নেই।

যদি বর্তমান লেয়ারে রিকভার করার সুযোগ থাকে, তবে রিকভার করুন:

JavaScript

try {
  await saveSettings(settings);
} catch (error) {
  showMessage("Settings could not be saved.");
  reportError(error);
}

আর যদি রিকভার করা সম্ভব না হয়, তবে এররের সাথে দরকারি কনটেক্সট যুক্ত করে সেটাকে উপরের লেয়ারে পাস করে দিন:

JavaScript

try {
  await saveSettings(settings);
} catch (error) {
  throw new Error("Failed to save account settings", {
    cause: error,
  });
}

অবশ্য কিছু ক্ষেত্রে এরর ইগনোর করাটা ইচ্ছাকৃত হতে পারে। যেমন ক্লিনআপ কোড হয়তো এমন কোনো টেম্পোরারি ফাইল রিমুভ করার চেষ্টা করতে পারে যা আগে থেকেই ডিলিট হয়ে গেছে।

JavaScript

try {
  await removeTemporaryFile(path);
} catch (error) {
  if (!(error instanceof FileNotFoundError)) {
    throw error;
  }
}

এটা কিন্তু অন্ধের মতো এরর সালো (swallow) করা নয়। আপনি জেনে-বুঝে শুধুমাত্র নির্দিষ্ট একটি ফেইলিওর হ্যান্ডেল করছেন। মনে রাখবেন, একটি catch ব্লকের অস্তিত্ব থাকার পেছনে সবসময় সঠিক কারণ থাকা উচিত।

8. Use Error Boundaries for React rendering failures

রিঅ্যাক্ট অ্যাপ্লিকেশনগুলোতে আরেক ধরণের ফেইলিওর দেখা যায়, যেখানে JavaScript error handling কিছুটা ভিন্ন।

ইউআই (UI) রেন্ডার করার সময় কোনো কম্পোনেন্ট এরর থ্রো করতে পারে। একটি Error Boundary এই ফেইলিওরটিকে আলাদা করতে পারে এবং ব্রোকেন ট্রি-এর বদলে একটি ফলব্যাক ইউআই (Fallback UI) দেখাতে পারে।

JavaScript

import { Component } from "react";

class ErrorBoundary extends Component {
  // Error Boundary Code 
  // (Previously explained above)
}

যে ফেইলিওরগুলোর জন্য Error Boundary ডিজাইন করা হয়েছে, শুধু সেখানেই এটি ব্যবহার করুন। আর অপারেশনাল async ফেইলিওরগুলো যেখানে ঘটছে, সেখানেই হ্যান্ডেল করুন।

9. Treat Node.js process-level handlers as emergency exits

একটি Node.js সার্ভিস এমন সব এরর শুনতে পারে যা সাধারণ অ্যাপ্লিকেশন হ্যান্ডেলিংয়ের বাইরে চলে গেছে।

JavaScript

process.on("uncaughtException", (error) => {
  console.error("Uncaught exception:", error);
  reportError(error);
  process.exit(1);
});

এখানের সবচেয়ে গুরুত্বপূর্ণ বিষয় হলো আপনি এখানে কী করছেন না।

কখনোই uncaughtException এর ভেতর থেকে নরমাল বিজনেস লজিক কন্টিনিউ করার চেষ্টা করবেন না। প্রসেস-লেভেল হ্যান্ডেলারগুলো শুধুমাত্র ফাইনাল লগিং, রিপোর্টিং এবং অ্যাপ্লিকেশন শাটডাউন করার জন্যই ব্যবহার করা উচিত।

প্রসেস-লেভেল হুকগুলো হলো আপনার আর্কিটেকচারের একেবারে শেষের সেফটি নেট, এরা কোনোভাবেই মূল JavaScript error handling এর বিকল্প নয়। আপনার রিকোয়েস্ট হ্যান্ডেলারদের উচিত প্রত্যাশিত ফেইলিওরগুলো লোকালি হ্যান্ডেল করা।

10. Use optional chaining for missing data, not broken assumptions

যখন কোনো ডেটা মিসিং থাকার সম্ভাবনা থাকে, তখন অপশনাল চেইনিং (Optional Chaining) এবং নালিশ কোয়ালিসিং (nullish coalescing) অপ্রয়োজনীয় ক্র্যাশ রোধ করতে সাহায্য করে।

JavaScript

const displayName = user?.profile?.displayName ?? "Anonymous";

তবে এই ডিফেন্সিভ সিনট্যাক্স খুব বেশি ব্যবহার করলে তা অনেক সময় বড় বাগ লুকিয়ে ফেলতে পারে।

অপশনাল চেইনিং এই প্রশ্নের উত্তর দেয়: কোনো ভ্যালু মিসিং থাকলে কী হওয়া উচিত? আর JavaScript error handling ভিন্ন একটি প্রশ্নের উত্তর দেয়: এই অপারেশনটি যদি ঠিকভাবে কন্টিনিউ করতে না পারে, তাহলে কী হওয়া উচিত? তারা একসাথে খুব ভালো কাজ করে, কিন্তু তারা সম্পূর্ণ আলাদা দুটি সমস্যার সমাধান করে।

A practical JavaScript error handling strategy

ইন্ডিভিজ্যুয়াল প্যাটার্নগুলো জানাটা যেমন জরুরি, তেমনি পুরো অ্যাপ্লিকেশনে JavaScript error handling কনসিস্টেন্সি বজায় রাখাও সমান গুরুত্বপূর্ণ। বেশিরভাগ রিয়েল-লাইফ অ্যাপ্লিকেশনের ক্ষেত্রে একটি সুন্দর স্ট্র্যাটেজি দেখতে এমন হয়:

  • Expected missing value → return null / undefined / Result
  • Recoverable operation failure → লোকালি catch করুন এবং ফলব্যাক দেখান
  • Unexpected lower-level failure → কনটেক্সট যুক্ত করে পুনরায় throw করুন
  • Rendering failure → Error Boundary ব্যবহার করুন
  • Unhandled application failure → গ্লোবাল রিপোর্টিং এ পাঠান
  • Unrecoverable Node.js process failure → লগ করুন → রিপোর্ট করুন → প্রসেস শাটডাউন করুন → রিস্টার্ট করুন

আপনার প্রতিটি ফাংশনের চারদিকে try/catch বসানোর কোনো প্রয়োজন নেই। আপনার দরকার হলো ফেইলিওরের সঠিক ওনারশিপ বা মালিকানা নিশ্চিত করা। এই সিদ্ধান্তগুলোই আপনার অ্যাপ্লিকেশনের এরর-হ্যান্ডেলিং কন্ট্রাক্ট তৈরি করে।

Final thoughts

একটি ভালো JavaScript error handling ব্যবস্থা তখনই সফল যখন সবকিছু ঠিকঠাক চললে তা একেবারে অদৃশ্য বা ইনভিজিবল থাকে। এর আসল মর্ম বোঝা যায় যখন কোনো কিছু নষ্ট হয়।

একটি ফাঁকা সাদা স্ক্রিনের বদলে ইউজার একটি ফলব্যাক বা মেসেজ দেখতে পায়। প্রোডাকশনে আনএক্সপেক্টেড বাগের বদলে আপনি কনটেক্সট-সহ একটি পরিষ্কার স্ট্যাক ট্রেস দেখতে পান।

আমাদের মূল লক্ষ্য প্রতিটি সম্ভাব্য এরর ক্যাচ করা নয়। বরং আমাদের লক্ষ্য হলো ফেইলিওরগুলোকে প্রেডিক্টেবল (predictable) করে তোলা।

যে এররগুলো থেকে রিকভার করা সম্ভব, শুধু সেগুলোই ক্যাচ করুন। যেখানে সম্ভব নয়, সেখানে কনটেক্সট যোগ করুন।

অরিজিনাল এরর কজ (cause) সংরক্ষণ করুন, সাইলেন্ট catch ব্লক এড়িয়ে চলুন এবং গ্লোবাল হ্যান্ডেলারগুলোকে শুধুমাত্র একদম শেষের সেফটি নেট হিসেবে ব্যবহার করুন।

এর মাধ্যমে আপনার কোড শুধুমাত্র “ক্র্যাশ করে না” এমন পর্যায়ে থাকবে না, বরং যখন কোনো কারণে এটি ক্র্যাশ করবে, তখন এটি ঠিকভাবে আপনাকে জানিয়ে দেবে যে আসলে কোথায় এবং কী সমস্যা হয়েছিল।

All Tech Update

Technology এর সকল আপডেট সবার আগে বিস্তারিত পড়ুন –

Scroll to Top