معرفی عملگر «انتساب ایمن» در ECMAScript جایگزین بلوک های Try-Catch

این پیشنهاد، یک عملگر جدید به نام `?=` (انتساب ایمن) را معرفی میکند که مدیریت خطاها را سادهتر میکند. این عملگر نتیجه یک تابع را به یک تاپل تبدیل میکند. اگر تابع خطایی ایجاد کند، این عملگر`[error, null]` را بازمیگرداند؛ و اگر تابع به درستی اجرا شود، `[null, result]` را برمیگرداند. این عملگر با `promise`ها، توابع `async` و هر مقداری که متد `Symbol.result` را پیادهسازی کرده باشد، سازگار است.

مثال:

هنگام انجام عملیات ورودی/خروجی یا تعامل با APIهای مبتنی بر Promise، خطاهایی ممکن است در زمان اجرا بهطور غیرمنتظره رخ دهند. عدم مدیریت این خطاها میتواند منجر به رفتارهای غیرمنتظره و آسیبپذیریهای امنیتی شود.

const [error, response] ?= await fetch(&quothttps://arthur.place&quot)

انگیزه جایگزینی بلوک های try catch با عملگر تخصیص ایمن:

  • 1. سادهسازی مدیریت خطا: حذف نیاز به بلوکهای `try-catch`.
  • 2. افزایش خوانایی: کاهش تو در تویی کد و بهبود جریان مدیریت خطاها.
  • 3. سازگاری بین APIها: ایجاد یک روش یکنواخت برای مدیریت خطا در APIهای مختلف.
  • 4. بهبود امنیت: کاهش خطر نادیده گرفتن مدیریت خطا و بهبود امنیت کلی کد.

مشکلی که این پیشنهاد حل میکند:

کد زیر ممکن است خطاهایی بدون هشدار صریح ایجاد کند:

async function getData() {
const response = await fetch(&quothttps://api.example.com/data&quot)
const json = await response.json()
return validationSchema.parse(json)
}

- فتچ `fetch` میتواند شکست بخورد.

- جیسون `json` میتواند خطا داشته باشد.

- پارسه `parse` ممکن است خطا ایجاد کند.

برای رفع این مسئله، عملگر `?=` پیشنهاد شده است که مدیریت خطا را کوتاه و خوانا میکند:

async function getData() {
const [requestError, response] ?= await fetch(&quothttps://api.example.com/data&quot)
if (requestError) {
handleRequestError(requestError)
return
}
const [parseError, json] ?= await response.json()
if (parseError) {
handleParseError(parseError)
return
}
const [validationError, data] ?= validationSchema.parse(json)
if (validationError) {
handleValidationError(validationError)
return
}
return data
}

ویژگیهای پیشنهادی:

1. سیمبول دات ریزالت Symbol.result: هر ابجکتی که متد`Symbol.result` را پیادهسازی کند، میتواند با عملگر `?=` استفاده شود.

function example() {
return {
[Symbol.result]() {
return [new Error(&quot123&quot), null]
},
}
}
const [error, result] ?= example()

2. انتساب ایمن با `?=`: این عملگر متد`Symbol.result` را روی شیء یا تابع سمت راست فراخوانی میکند و خطاها و نتایج را بهصورت منظم مدیریت میکند.

const obj = {
[Symbol.result]() {
return [new Error(&quotخطا&quot), null]
},
}
const [error, data] ?= obj



استفاده با Promiseها:

یک Promise نیز میتواند با عملگر `?=` استفاده شود.

const promise = getPromise()
const [error, data] ?= await promise

این پیشنهاد همچنین با استفاده از`try-catch` تفاوت دارد، زیرا عملگر `?=` بهطور صریح خطاها را در جریان کد مدیریت میکند.



استفاده از دستور `using`

استفاده از دستور `using` یا `await using` نیز باید با عملگر `?=` سازگار باشد. این دستور بهطور مشابه با استفاده استاندارد از`using x = y` عمل خواهد کرد. توجه کنید که خطاهای ایجاد شده در هنگام آزادسازی منابع توسط عملگر `?=` گرفته نمیشوند، همانطور که در سایر ویژگیهای فعلی هم مدیریت نمیشوند.

try {
using a = b
} catch(error) {
// مدیریت خطا
}
// حالا به این صورت میشود
using [error, a] ?= b
// یا با async
try {
await using a = b
} catch(error) {
// مدیریت خطا
}
// حالا به این صورت میشود
await using [error, a] ?= b

جریان مدیریت `using` فقط زمانی اعمال میشود که`error` برابر با `null` یا `undefined` باشد و `a` دارای یک متد `Symbol.dispose` باشد.

بلوک `Try/Catch` کافی نیست

بلوک `try {}` اغلب مفید نیست، زیرا سطحبندی آن اهمیت مفهومی ندارد. در اغلب موارد، مانند یک توضیح به جای یک ساختار کنترل جریان عمل میکند. برخلاف بلوکهای کنترل جریان، هیچ حالت برنامهای وجود ندارد که فقط در داخل یک بلوک `try {}` معتبر باشد.

در مقابل، بلوک `catch {}` یک کنترل جریان واقعی است و سطحبندی آن معنادار و مرتبط است.

استفاده از بلوکهای`try/catch` دو مشکل دستوری عمده دارد:

- برای هر بلوک مدیریت خطا، یک سطح تو در تو ایجاد میکند.

- متغیرهایی را که در خارج از بلوک باید تعریف شوند، که مطلوب نیست، معرفی میکند.

به کد زیر نگاه کنید این دو مورد توضیح داده شده است:

// Nests 1 level for each error handling block
async function readData(filename) {
  try {
    const fileContent = await fs.readFile(filename, &quotutf8&quot)

    try {
      const json = JSON.parse(fileContent)

      return json.data
    } catch (error) {
      handleJsonError(error)
      return
    }
  } catch (error) {
    handleFileError(error)
    return
  }
}

// Declares reassignable variables outside the block, which is undesirable
async function readData(filename) {
  let fileContent
  let json

  try {
    fileContent = await fs.readFile(filename, &quotutf8&quot)
  } catch (error) {
    handleFileError(error)
    return
  }

  try {
    json = JSON.parse(fileContent)
  } catch (error) {
    handleJsonError(error)
    return
  }

  return json.data
}

چرا دادهها اول نیستند؟

در زبان Go، قرار دادن دادهها قبل از خطا مرسوم است، و ممکن است بپرسید چرا این روش در جاوااسکریپت استفاده نمیشود. در جاوااسکریپت، ما قبلاً این امکان را داریم که بهطور ساده `const data = fn()` بنویسیم و خطاها را نادیده بگیریم که دقیقاً همان مشکلی است که تلاش داریم حل کنیم.

اگر کسی از عملگر `?=` استفاده میکند، به این دلیل است که میخواهد مطمئن شود که خطاها را مدیریت کرده و آنها را نادیده نمیگیرد. قرار دادن دادهها اول، این اصل را نقض میکند، زیرا نتیجه را بر مدیریت خطاها اولویت میدهد.

// خطاها را نادیده میگیرد!
const data = fn()
// به سادگی فراموش میکنید که خطا را مدیریت کنید
const [data] ?= fn()
// این روش صحیح است
const [error, data] ?= fn()


اگر میخواهید خطا را نادیده بگیرید (که متفاوت از نادیده گرفتن احتمال وجود خطا در یک تابع است)، میتوانید بهسادگی این کار را انجام دهید:

// این خطا را نادیده میگیرد (آن را نادیده میگیرد و مجدداً آن را پرتاب نمیکند)
const [, data] ?= fn()


این روش بسیار واضحتر و قابلفهمتر است، زیرا اذعان دارد که ممکن است خطایی وجود داشته باشد، اما نشان میدهد که شما اهمیتی به آن نمیدهید.

Polyfilling

این پیشنهاد را میتوان با استفاده از کد موجود در فایل `polyfill.` پالیفیل کرد.

با این حال، خود عملگر `?=` مستقیماً نمیتواند پالیفیل شود. هنگام هدفگذاری محیطهای قدیمیتر جاوااسکریپت، باید از یک پردازشگر پس از پردازش برای تبدیل عملگر `?=` به فراخوانیهای متناظر`Symbol.result` استفاده کنید.


const [error, data] ?= await asyncAction(arg1, arg2)
// به این تبدیل شود
const [error, data] = await asyncAction[Symbol.result](arg1, arg2)


استفاده از `?=` با توابع و اشیائی که متد `Symbol.result` ندارند

اگر تابع یا شیء `Symbol.result` را پیادهسازی نکند، عملگر `?=` باید یک`TypeError` پرتاب کند.

مقایسه

عملگر `?=` و پیشنهاد`Symbol.result` منطق جدیدی به زبان اضافه نمیکنند. در واقع، همه آنچه که این پیشنهاد قصد دارد به دست آورد، در حال حاضر با استفاده از ویژگیهای زبان فعلی (گرچه با روشهای طولانی و مستعد خطا) امکانپذیر است.

try {
// try expression
} catch (error) {
// catch code
}

یا

promise // try expression
.catch((error) => {
// catch code
})


معادل است با:

const [error, data] ?= expression
if (error) {
// catch code
} else {
// try code
}