بازگشت به دوره

آشنایی با ساختار try-catch و خطاهای سفارشی

در این بخش به معرفی ساختار try-catch می‌پردازیم که به منظور مدیریت خطا (ٍError Handling) یا مدیریت استثنا (Exception Handling) به کار می‌رود. همچنین با نحوه‌ی تولید خطاهای سفارشی با کلمه‌ی کلیدی throw آشنا می‌شویم. لازم به ذکر است که کاربرد اصلی مباحث مطرح شده در این بخش در برنامه‌های بزرگ و پیچیده است. به همین دلیل درک اهمیت این مباحث با چند مثال ساده، امکان‌پذیر نیست. اما به مرور که تجربه‌ی بیشتری در برنامه‌نویسی کسب می‌کنید و برنامه‌های بزرگتری می‌نویسید، اهمیت این مباحث برای شما بیشتر روشن خواهد شد.

 

ساختار try-catch

در صورتی که بخشی از کدهای برنامه مستعد بروز خطا باشند، می‌توان آن بخش را در یک بلاک try قرار داد. با این کار در صورت بروز خطا در این کدها، برنامه متوقف نخواهد شد. بلکه در صورت وجود یک بلاک catch پس از بلاک try، مفسر جاوا اسکریپت از بلاک try خارج شده و بلاک catch را اجرا می‌کند.

البته با توجه به اینکه برنامه در این حالت متوقف نشده و می‌تواند ادامه پیدا کند، به جای به کار بردن کلمه‌ی "خطا" بهتر است از کلمه‌ی "استثنا" استفاده می‌کنیم. به عبارت دیگر یک خطا زمانی رخ می‌دهد که هیچ تدبیری برای مدیریت یک استثنا اندیشیده نشده باشد. با این حال این دو کلمه معمولاً به جای یکدیگر به کار برده می‌شوند. اما در این بخش سعی می‌کنیم تا حد ممکن بین این دو مفهوم تمایز قائل شویم.

به عنوان مثال به دستورات زیر توجه کنید. در این دستورات ابتدا یک عدد به عنوان طول آرایه از کاربر دریافت می‌شود. سپس یک آرایه‌ی جدید ایجاد شده و مقدار وارد شده توسط کاربر به عنوان طول این آرایه (خاصیت length) به کار برده می‌شود.


try{
	let size = prompt('طول آرایه را وارد کنید');
	const arr = new Array();
	arr.length = size;
}catch(error){
	alert('لطفاً یک عدد صحیح مثبت را وارد کنید');
}

با اجرای این برنامه، در صورتی که مقدار وارد شده در دیالوگ prompt یک عدد صحیح غیر منفی نباشد، یک استثنا رخ خواهد داد. زیرا طول آرایه‌ها باید حتماً یک عدد صحیح و غیر منفی باشد. اما با توجه به اینکه این استثنا در بلاک try رخ داده است، برنامه متوقف نمی‌شود. بلکه اجرای بلاک try متوقف می‌شود و بلاک catch اجرا می‌شود. پس از اجرای بلاک catch نیز، روند عادی اجرای برنامه ادامه خواهد یافت. این برنامه را می‌توانید اینجا اجرا کنید.

نکته : مشخصات استثنای رخ داده در بلاک try به صورت یک شئ به بلاک catch ارسال می‌شود. در بلاک catch می‌توان از این شئ با نامی دلخواه که در مقابل کلمه‌ی کلیدی catch قرار می‌گیرد استفاده کرد که در این مثال از نام error استفاده شده است. توجه کنید که حتی اگر نیازی به استفاده از این شئ در بلاک catch نداشته باشید (مانند همین مثال)، باز هم باید نام آن را تعریف کنید. در غیر این صورت در برخی مرورگرها (مثلا Edge) با خطا مواجه خواهید شد.

شئ error دارای خاصیت‌هایی است که اطلاعاتی را در مورد نوع استثنا نگهداری می‌کنند. این خاصیت‌ها در مرورگرهای مختلف کمی متفاوت هستند. اما دو خاصیت message و name در تمام مرورگرها پشتیبانی می‌شوند.

خاصیت name نوع خطا را مشخص می‌کند و مقدار آن در تمام مرورگرها یکسان است. مثلا در مثال فوق مقدار این خاصیت "RangeError" است. خاصیت message نیز پیامی را در مورد خطا نگهداری می‌کند که در مرورگرهای مختلف کمی متفاوت است. مثلا در مورد مثال فوق و در مرورگر Chrome، مقدار این خاصیت "Invalid array length" می‌باشد. از این خاصیت‌ها می‌توان در نمایش اطلاعات به کاربر استفاده کرد. نمونه‌ای از این کار را در ادامه‌ی این بخش خواهیم دید.

یک بلاک try ممکن است مستعد وقوع انواع متفاوتی از استثناها باشد. اما به ازای هر بلاک try فقط می‌توان از یک بلاک catch استفاده کرد. پس باید راهی برای تشخیص نوع استثنا در بلاک catch وجود داشته باشد تا تصمیم مناسبی با توجه به نوع استثنا اتخاذ شود. هر چند برای این منظور می‌توان از خاصیت name از شئ error استفاده کرد، اما معمولاً از این روش استفاده نمی‌شود. بلکه روش مرسوم برای این کار استفاده از عملگر instanceof است.

 

عملگر instanceof

پیش از این با عملگر typeof آشنا شده‌ایم. با استفاده از این عملگر می‌توان نوع داده‌ی ذخیره شده در یک متغیر را به دست آورد. اما متاسفانه این عملگر به غیر از توابع و انواع داده‌ی اولیه، برای سایر انواع داده‌ها مقدار "object" را بازمی‌گرداند. در نتیجه با این عملگر نمی‌توان نوع اشیاء را تشخیص داد. به عبارت دیگر داده‌ای که از نوع Array باشد، با داده‌ای که از نوع Date است، از نظر این عملگر یکسان بوده و هر دو "object" هستند.

با استفاده از عملگر instanceof می‌توان مشکل عملگر typeof را حل کرد. یعنی می‌توان نوع اشیاء را نیز تشخیص داد. توجه کنید که این عملگر بر خلاف عملگر typeof، نوع داده را به صورت یک رشته باز نمی‌گرداند. بلکه مقدار بازگشتی این عملگر true یا false است. دستورات زیر چند نمونه از خروجی‌های تولید شده با استفاده از عملگر instanceof را نشان می‌دهد.


const a = new Array();
const b = new Object();
const c = new Date();
a instanceof Array;
← true
a instanceof Date;
← false
b instanceof Array;
← false
b instanceof Object;
← true
c instanceof Array;
← false
c instanceof Object;
← true
c instanceof Date;
← true

توجه کنید که دستور "c instanceof Object" مقدار true را بازمی‌گرداند. این در حالی است که متغیر c حاوی شیئی از نوع Date است. این رفتار به دلیل وجود مفهومی به نام "وراثت" در برنامه‌نویسی شئ‌گرا بروز می‌کند. با توجه به اینکه تمام اشیاء از هر نوعی که باشند، تمام ویژگی‌های شئ Object را به "ارث" می‌برند. در نتیجه تمام اشیاء نوعی Object نیز هستند. در مورد وراثت که یکی از مفاهیم بسیار مهم در برنامه‌نویسی شئ‌گرا محسوب می‌شود در فصل ۱۲ صحبت خواهیم کرد.

 

تشخیص نوع استثنا

به غیر از خطاهای منطقی، به ازای تمام انواع خطاهایی که در بخش قبل معرفی شدند، یک شئ در جاوا اسکریپت وجود دارد. این اشیاء دقیقاً همنام با نوع خطا‌ها هستند (مثلاً شئ TypeError یا شئ ReferenceError). به همین دلیل در زمان وقوع یک استثنا در بلاک try، با توجه نوع استثنا، یک شئ از همان نوع به عنوان شئ error به بلاک catch ارسال می‌شود. در نتیجه در بلاک catch می‌توان با استفاده از عملگر instaceof نوع استثنا را تشخیص داد.

به عنوان مثال به برنامه‌ی زیر توجه کنید. در این برنامه مانند مثال قبل ابتدا برای ساختن یک آرایه‌ی جدید، طول آرایه از کاربر پرسیده می‌شود. سپس نام یک متد از شئ Math از کاربر پرسیده می‌شود. کاربر می‌تواند نام‌هایی مانند "floor" و "sign" که متدهایی از شئ Math هستند را وارد کند. در دستور بعدی عدد ۱۰ به عنوان آرگومان ورودی به متد مشخص شده توسط کاربر ارسال می‌شود. سپس مقدار بازگشتی از این متد به کاربر نمایش داده می‌شود.


try{
	let size = prompt('طول آرایه را وارد کنید');
	const arr = new Array();
	arr.length = size;
	let method = prompt('نام یک متد را وارد کنید');
	let result = Math[method](10);
	alert(result);
}catch(error){
	if(error instanceof RangeError){
		alert('لطفاً یک عدد صحیح مثبت را وارد کنید');
	}else if(error instanceof TypeError){
		alert('لطفاً نام متد را صحیح وارد کنید');
	}
}

در این برنامه احتمال وقوع دو نوع استثنا وجود دارد. پس در بلاک catch ابتدا با استفاده از عملگر instanceof نوع استثنا تشخیص داده می‌شود. سپس با توجه به نوع استثنا، پیام مناسبی به کاربر نمایش داده می‌شود. قسمت اول این مثال دقیقاً مانند مثال قبل است و می‌تواند منجر به وقوع خطای محدوده شود. اما قسمت دوم که نام یک متد را دریافت می‌کند، می‌تواند منجر به وقوع یک خطای نوع شود. زیرا ممکن است کاربر یک نام نامعتبر را به عنوان متدی از شئ Math وارد کند. در بخش قبل دیدیم که فراخوانی متدی که وجود ندارد، منجر به وقوق خطای نوع می‌شود. به همین دلیل در قسمت catch برای بررسی وقوع این خطا، شئ error را با TypeError مقایسه می‌کنیم. این مثال را می‌توانید اینجا اجرا کنید.

 

بلاک finally

در ساختار try-catch می‌توان از بلاک دیگری به نام finally نیز استفاده کرد. در صورت استفاده از بلاک finally، استفاده از بلاک catch اختیاری است و می‌توان از بلاک catch استفاده نکرد. البته در عمل به ندرت چنین کاری انجام می‌شود و تقریباً همیشه از بلاک catch نیز استفاده می‌شود.

دستورات موجود در بلاک finally تحت هر شرایطی اجرا می‌شوند. یعنی وقوع یا عدم وقوع یک استثنا در بلاک try، هیچ تاثیری در اجرای بلاک finally ندارد. پس می‌توان دستوراتی که اجرای آنها ضروری است را در بلاک finally قرار داد. البته ساختار try-catch معمولاً بدون بلاک finally به کار برده می‌شود.

نکته‌ی مهمی که در مورد بلاک finally وجود دارد این است که در صورت استفاده از این بلاک، مفسر از تمام دستورات return که در بلاک try یا بلاک catch به کار رفته باشند صرف نظر می‌کند. زیرا اجرای دستور return موجب خروج از تابع و عدم اجرای بلاک finally می‌شود. به عنوان مثال در دستورات زیر مقدار برگشتی از تابع example همیشه برابر با ۳ است و وقوع یا عدم وقوع استثنا هیچ تاثیری در مقدار بازگشتی ندارد.


function example(){
	try{
		// دستورات دلخواه
		return 1;
	}catch(error){
		// دستورات دلخواه
		return 2;
	}finally{
		return 3;
	}
}
 

تولید خطای سفارشی (یا استثنای سفارشی)

با استفاده از کلمه‌ی کلیدی throw می‌توان در هر نقطه‌ای از برنامه یک استثنا تولید کرد. در صورتی که این استثنا در یک بلاک try تولید شده باشد، می‌توان در بلاک catch آن را مدیریت کرد. اما در صورتی که استثنا خارج از بلاک try تولید شده باشد، موجب بروز خطا و توقف برنامه می‌شود. هر مقداری که مقابل کلمه‌ی کلیدی throw قرار داده شود، به عنوان شئ error به بلاک catch ارسال می‌شود. به عنوان مثال تمام دستورات زیر معتبر هستند.


throw 'Error2';
throw 42;
throw true;

البته معمولاً از اشیاء خطای استاندارد برای این منظور استفاده می‌شود. یعنی با توجه به نوع استثناء رخ داده، یک شئ از نوع مربوطه ایجاد می‌شود. به عنوان مثال تابع زیر را در نظر بگیرید که باید یک رشته را به عنوان ورودی دریافت کند. این تابع ابتدا نوع آرگومان ورودی را بررسی می‌کند. سپس در صورت نادرست بودن نوع ورودی، یک خطا از نوع TypeError ایجاد می‌کند. همچنین رشته‌ای که به این شئ ارسال می‌شود، در خاصیت message از شئ error قرار می‌گیرد.


function example(str){
	if(typeof str != "string"){
		throw new TypeError('Input type is wrong!');
	}
	// سایر دستورات تابع
}

اما تولید خطاهای سفارشی چه کاربردی دارد؟ همانطور که اشاره شد اهمیت مباحث این بخش در برنامه‌های بزرگ و پیچیده روشن می‌شود و با چنین مثال‌های ساده‌ای نمی‌توان اهمیت این مباحث را به خوبی توضیح داد. با این حال سعی می‌کنیم با یک مثال ساده کاربرد خطاهای سفارشی را بیان کنیم.

فرض کنید مشغول نوشتن یک کتابخانه‌ی جاوا اسکریپت نسبتاً بزرگ هستید که دارای توابع زیادی برای انجام عملیات ریاضی مختلف است. معمولاً چنین کتابخانه‌هایی توسط برنامه‌نویسان زیادی مورد استفاده قرار می‌گیرند. مسلماً تمام برنامه‌نویسان نمی‌توانند روش استفاده‌ی صحیح از تمام امکانات این کتابخانه را به خاطر بسپارند. به عنوان مثال ممکن است نوع آرگومان‌های ورودی و یا تعداد آرگومان‌های ورودی یک تابع را فراموش کنند.

فرض کنید یکی از توابع این کتابخانه mySqrt نام دارد که برای محاسبه‌ی ریشه‌ی دوم (جذر) یک عدد به کار می‌رود. می‌دانیم که اعداد منفی ریشه‌ی دوم حقیقی ندارند. اما ممکن است یک برنامه‌نویس این نکته را نداند و اعداد منفی را به این تابع ارسال کند. و یا حتی ممکن است نوع داده‌ی غیر عددی به این تابع ارسال کند. در چنین شرایطی برای جلوگیری از ادامه‌ی اجرای برنامه که می‌تواند منجر به وقوع خطاهای بعدی شود. و همچنین جهت مطلع کردن برنامه‌نویس از نحوه‌ی استفاده‌ی صحیح از این تابع می‌توان یک خطای سفارشی تولید کرد. مثال زیر نحوه‌ی نوشتن چنین تابعی را نشان می‌دهد. توجه کنید که از پیام‌های فارسی در تولید خطاها استفاده شده است. هرچند در کتابخانه‌هایی که در دسترس عموم قرار می‌گیرند بهتر است فقط از زبان انگلیسی استفاده شود. اما در کتابخانه‌های اختصاصی که فقط در یک محیط محدود (مثلاً یک شرکت) مورد استفاده قرار می‌گیرند، می‌توان از زبان‌های بومی نیز استفاده کرد.


function mySqrt(num){
	if(typeof num != "number"){
		throw new TypeError('ورودی این تابع حتماً باید یک عدد باشد');
	}else if(num < 0){
		throw new RangeError('اعداد منفی ریشه‌ی دوم حقیقی ندارند');
	}
	// سایر دستورات تابع
}

حال اگر برنامه‌نویس این تابع را در یک بلاک try با ورودی نامعتبر فراخوانی کند به دلیل تولید استثنا، بلاک catch اجرا می‌شود که همین امر موجب مطلع شدن برنامه‌نویس از وجود اشکال در برنامه می‌شود. همچنین اگر این تابع خارج از بلاک try با ورودی نامعتبر فراخوانی شود، پس از تولید خطا برنامه متوقف می‌شود. در نتیجه در این حالت نیز برنامه‌نویس متوجه وجود اشکال در کدها و وقوع خطا در اجرای برنامه می‌شود. ضمناً در این حالت برنامه‌نویس می‌تواند با مراجعه به بخش Console، پیام خطای صادر شده توسط تابع mySqrt را مشاهده کند. شکل زیر وضعیت Console را در مرورگر Chrome در صورت ارسال یک عدد منفی به تابع mySqrt نشان می‌دهد.

throw-exception

مشاهده می‌کنید که هم نوع خطا، هم پیام مناسب و هم محل وقوع خطا در کنسول نمایش داده می‌شود.

استفاده از خطاهای سفارشی در برنامه‌هایی که به صورت تیمی نوشته می‌شوند، می‌تواند بسیار مفید باشد. به طور کلی در نوشتن برنامه‌هایی که ممکن است توسط سایر برنامه‌نویسان مورد استفاده قرار گیرند، استفاده از خطاهای سفارشی اکیداً توصیه می‌شود.