خطا پرتاب کنیم، Result برگردونیم، یا هیچکدوم؟
خلاصهٔ کاملتر
نویسنده میگه پرتکرارترین سؤالی که دربارهٔ کد رویدادمحورش میشنوه اینه: چرا از Result استفاده نمیکنی؟ معمولاً کسی میبینه یه تصمیم دامنه استثنا پرتاب میکنه و پیشنهاد میده شکست تو Result بره، try/catch بمونه لب مرز اپلیکیشن و تفاوتها هم تو نوع خروجی صریح بشه. به گفتهٔ خودش، مشکلی با استثنا نداره، با Result هم تو زبانی که این الگو بومیشه مشکلی نداره — ولی نوع خروجی فقط بخشی از جوابه.
نقطهٔ شروع بحث اینه که بعضی شکستها ارزش نگه داشتن بهعنوان دادهٔ کسبوکار رو دارن. درخواست کالای ناموجود میتونه به گزارش «تقاضای برآوردهنشده» غذا بده، پرداخت ردشده میتونه یه فرایند دیگه رو راه بندازه و سقف تعداد اقلام میتونه توضیح بده چرا مشتریها وسط کار بیخیال میشن. نویسنده به مفهومی از گویکو آدزیچ به اسم lizard optimization اشاره میکنه: استفادهٔ نظاممند از الگوهای استفادهٔ غیرمنتظره برای بهتر کردن محصول. اگه هر تلاش ناموفق فقط به یه پاسخ HTTP یا لاگ استثنا تبدیل بشه، پیدا کردن این الگوها سخت میشه.
پس روی میز سه گزینه هست: پرتاب خطا و گرفتنش لب مرز، برگردوندن Result با شکست روی شاخهٔ خطا، یا برگردوندن یه رویداد مثل ProductItemOutOfStock و تصمیمگیری جداگانه دربارهٔ اینکه باهاش چی کار کنیم. جواب نویسنده تقریباً هیچوقت «فقط پرتاب کن» یا «فقط Result برگردون» نیست؛ اول باید بدونه چی باید ذخیره بشه، فراخوان چی باید بگیره و آیا این شکست اصلاً به مسیر استثنای اپلیکیشن تعلق داره یا نه.
تو مدل رویدادی، تصمیم افزودن کالا به سبد میتونه سه رویداد متمایز برگردونه و هرکدوم دادههای خودشو حمل کنه: ProductItemOutOfStock تعداد درخواستی و موجودی رو داره و ShoppingCartItemLimitReached سقف تنظیمشده و محصولی که ازش رد شده. نویسنده میگه میشد یه رویداد کلیِ «افزودن ناموفق» با فیلد reason هم ساخت، ولی اونوقت دقت و ریزدانگی اطلاعات از دست میرفت؛ با تایپهای جدا، یه پروجکشن یا تست میتونه مستقیم همون نتیجهٔ مربوطه رو چک کنه.
در مقابل، هر شکستی رویداد کسبوکاری نیست. سبد تأییدشده دیگه قابل تغییر نیست، فرمان بدون دادهٔ لازم نامعتبره، و در دسترس نبودن event store یا تایماوت سرویس موجودی یعنی عملیات اصلاً قابلاتکا انجام نشده — اینها جاشون استثناست. نکتهٔ ظریفش اینه که پرتاب قبل از تولید رویداد اتفاق میافته، پس هندلر چیزی برای الحاق به استریم نداره. ضمناً هر زبانی، حتی Rust، میتونه پنیک یا خطای غیرمنتظره بده، پس اپلیکیشن بههرحال یه مرز خطا لازم داره.
دربارهٔ خود Result، نویسنده معتقده تو این مثال چیزی جز یه دستهبندی موفق/ناموفق دور همون سه رویداد اضافه نمیکنه: اگه بخوایم بر اساس نوع شکست پاسخ متفاوتی بدیم، باز همون switch لازمه. یعنی تصمیم حذف نشده، فقط نادیده گرفتنش راحتتر شده. بهعلاوه تو محیطهایی مثل TypeScript که عملگر pipe ندارن، این الگو یا شاخههای تکراری تو کل زنجیرهٔ فراخوانی میسازه یا یه کتابخانه با واژگان غیربومی و پرسروصدا. نویسنده یادآوری میکنه حتی اسکات ولاشین، مروج railway-oriented programming، هم گفته Result برای مدلکردن جایگزینهای انتظاررفته ساخته شده، نه برای پیچیدن دور هر تابع.
نکتهٔ کلیدی بعدی اینه که برگردوندن یه رویداد به معنی ذخیره کردنش نیست. اگه کسبوکار به ثبت «تقاضای برآوردهنشده» اهمیت بده، اون رویداد باید پایدار بشه؛ ولی اگه فقط بخوایم لاگش کنیم یا تو پاسخ HTTP برش گردونیم، الحاقش به استریم فقط دادهٔ بیمصرف نگه میداره. نویسنده اسم این الگو رو persistence انتخابی میذاره: تصمیم دامنه نتیجه رو توصیف میکنه و اپلیکیشن تصمیم میگیره کدوم نتیجه ماندگار بشه. تو فریمورک Emmett این با میانافزار تصمیم پیاده شده:
middleware: [
skipOn((event) => event.type === "ProductItemOutOfStock"),
stopOn((event) => event.type === "ShoppingCartItemLimitReached"),
rejectOn((event) => event.type === "ShoppingCartWasAlreadyConfirmed"),
],جایی که این تفاوتها واقعاً خودشونو نشون میدن، سناریوی ورود دستهای اطلاعاته. وقتی درخواست از رابط کاربری خودمون میآد، ما مرجعیم و پرتاب استثنا قابلقبوله. ولی تو import، سیستم بیرونی فقط داره خبر میده سبدش چه خطوطی داشته؛ این حرف دربارهٔ اون سیستم درسته ولی معنیاش این نیست که مدل ما میتونه قبولش کنه. رویدادهای برگشتی نشون میدن کدوم خطوط قبل از توقف پذیرفته شدن و ایمپورتر میتونه همون رکورد رو «در انتظار موجودی» یا «ردشده» علامت بزنه و بره سراغ رکورد بعدی — کاری که با پرتاب استثنا اصلاً ممکن نبود، چون پردازش کلاً متوقف میشد.
نکات کلیدی:
- سه گزینه برای شکست: پرتاب استثنا، برگردوندن Result، یا برگردوندن رویداد و تصمیمگیری جداگانه
- شکستهایی که ارزش تحلیلی دارن (کالای ناموجود، پرداخت ردشده) بهتره رویداد باشن تا بشه روشون پروجکشن و گزارش ساخت
- قانون شکستهشده، فرمان نامعتبر و خرابی زیرساخت جاشون تو استثناست؛ پرتاب قبل از تولید رویداد رخ میده
- Result اینجا فقط دستهبندی موفق/ناموفق اضافه میکنه و همون switch روی نوع شکست باقی میمونه
- «persistence انتخابی»: تصمیم دامنه نتیجه رو توصیف میکنه، اپلیکیشن تصمیم میگیره چی ماندگار بشه
- تو پردازش دستهای و هندلرهای ناهمگام، پرتاب استثنا امکان ادامه دادن و بازیابی رو از بین میبره




