جاوااسکریپت - اصلاح نحو Try...Catch با استفاده از Async/Await


من به سطحی بودن ویژگی جدید Async/Await موجود در Typescript و غیره علاقه دارم. با این حال، مطمئن نیستم که از این واقعیت خوشم بیاید که باید متغیری که روی آن await میکنم را خارج از بلوک try...catch اعلام کنم تا بتوانم بعداً از آن استفاده کنم. به این صورت:

let createdUser
try {
    createdUser = await this.User.create(userInfo)
} catch (error) {
    console.error(error)
}

console.log(createdUser)
// business
// logic
// goes
// here

لطفاً اگر اشتباه میکنم مرا تصحیح کنید، اما به نظر میرسد بهترین روش این است که چندین خط از منطق کسبوکار را در بدنه try قرار ندهیم، بنابراین تنها جایگزین من این است که createdUser را خارج از بلوک اعلام کنم، آن را در بلوک اختصاص دهم و سپس بعد از آن از آن استفاده کنم.

بهترین روش در این مورد چیست؟

پاسخ شماره ۱

به نظر میرسد بهترین روش این است که چندین خط منطق کسبوکار را در بدنه try قرار ندهیم

در واقع من میگویم که هست. معمولاً شما میخواهید catch تمام استثناها را از کار با آن مقدار بگیرید:

try {
    const createdUser = await this.User.create(userInfo);

    console.log(createdUser)
    // business logic goes here
} catch (error) {
    console.error(error) // from creation or business logic
}

اگر میخواهید فقط خطاهای مربوط به وعده را گرفته و مدیریت کنید، سه گزینه دارید:

  • متغیر را بیرون اعلام کنید و بسته به اینکه استثنا رخ داده است یا نه، شاخهبندی کنید. این میتواند اشکال مختلفی داشته باشد، مانند

    • اختصاص یک مقدار پیشفرض به متغیر در بلوک catch
    • بازگشت زود هنگام یا پرتاب دوباره یک استثنا از بلوک catch
    • یک فلگ تنظیم کنید که آیا بلوک catch یک استثنا را گرفته است یا خیر، و آن را در یک شرط if آزمایش کنید
    • آزمون برای اینکه متغیر مقداردهی شده باشد
      let createdUser; // or use `var` inside the block
      try {
          createdUser = await this.User.create(userInfo);
      } catch (error) {
          console.error(error) // from creation
      }
      if (createdUser) { // user was successfully created
          console.log(createdUser)
          // business logic goes here
      }
    
  • نوع استثنای گرفتهشده را بررسی کنید و بر اساس آن آن را مدیریت یا دوباره پرتاب کنید.

      try {
          const createdUser = await this.User.create(userInfo);
          // user was successfully created
          console.log(createdUser)
          // business logic goes here
      } catch (error) {
          if (error instanceof CreationError) {
              console.error(error) // from creation
          } else {
              throw error;
          }
      }
    

    متأسفانه، جاوااسکریپت استاندارد (هنوز) از نحو برای استثناهای شرطی پشتیبانی نمیکند.

    اگر روش شما پرامیسهایی که با خطاهای به اندازه کافی مشخص رد شدهاند را برنمیگرداند، میتوانید خودتان این کار را با دوباره پرتاب کردن چیزی مناسبتر در یک هندلر .catch() انجام دهید:

      try {
          const createdUser = await this.User.create(userInfo).catch(err => {
              throw new CreationError(err.message, {code: "USER_CREATE"});
          });
          …
      } …
    

    همچنین مشاهده کنید مدیریت چندین catch در زنجیره وعده برای نسخه پیش از async/await این.

  • به جای try/catch از then با دو callback استفاده کنید. این واقعاً کمزشتترین روش است و همچنین توصیه شخصی من برای سادگی و درستی آن است، بدون اینکه به خطاهای برچسبزده شده یا ظاهر مقدار نتیجه برای تمایز بین برآورده شدن و رد شدن promise تکیه کند:

      await this.User.create(userInfo).then(createdUser => {
          // user was successfully created
          console.log(createdUser)
          // business logic goes here
      }, error => {
          console.error(error) // from creation
      });
    

    البته این با معایبی همراه است که شامل معرفی توابع بازخوانی (callback) میشود، به این معنی که دیگر نمیتوانید به راحتی حلقهها را با break/continue قطع کنید یا زودهنگام return از تابع بیرونی انجام دهید.

پاسخ شماره ۲

یک روش سادهتر دیگر این است که .catch را به تابع promise اضافه کنید. مثال:

const createdUser = await this.User.create(userInfo).catch( error => {
// handle error
})

پاسخ شماره ۳

کد تمیزتر

استفاده از async/await همراه با هندلر catch پرامیس.

از آنچه میبینم، این مشکلی قدیمی است که بسیاری از برنامهنویسان و کدهای آنها را آزار داده است (هر دو معنی). متد .catch در Promise واقعاً تفاوتی با try/catch ندارد.

دستگیرهی catch در پرامیسهای ES6 به صورت همزمان با 'await/async' کار میکند و یک راهحل مناسب و کد تمیزتر ارائه میدهد:

const createUser = await this.User
    .create(userInfo)
    .catch(error => console.error(error))

console.log(createdUser)
// business
// logic
// goes
// here

توجه داشته باشید که در حالی که این به سؤال پاسخ میدهد، خطا را نادیده میگیرد. در این مورد، بهتر است خطا پرتاب شود زیرا (1) انتظار نمیرود این عملیات (ایجاد کاربر) شکست بخورد، و (2) به هر حال نمیتوانید ادامه دهید -- عملیات با خطا مواجه شده است:

const createUser = await this.User
    .create(userInfo)
    .catch(error => {
        // do what you need with the error
        console.error(error)

        // maybe send to Datadog or Sentry

        // don't gobble up the error
        throw error
    })

console.log(createdUser)
// business
// logic
// goes
// here

یادگیری catch ارزشش را ندارد؟

مزایای پاکیزگی ممکن است در نگاه اول آشکار نباشند، اما در عملیات پیچیده و غیرهمزمان واقعی جمع میشوند.

به عنوان یک مثال، علاوه بر ایجاد کاربر (this.User.create)، میتوانیم اعلان ارسال کنیم (this.pushNotification) و ایمیل بفرستیم (this.sendEmail).

this.User.create

this.User.create = async(userInfo) => {

    // collect some fb data and do some background check in parallel
    const facebookDetails = await retrieveFacebookAsync(userInfo.email)
        .catch(error => {
            // we can do some special error handling

            // and throw back the error
         })
    const backgroundCheck = await backgroundCheckAsync(userInfo.passportID)

    if (backgroundCheck.pass !== true) throw Error('Background check failed')

    // now we can insert everything
    const createdUser = await Database.insert({ ...userInfo, ...facebookDetails })

    return createdUser
}

this.pushNotifcation و this.sendEmail

this.pushNotification = async(userInfo) => {
    const pushed = await PushNotificationProvider.send(userInfo)
    return pushed
})

this.sendEmail = async(userInfo) => {
    const sent = await mail({ to: userInfo.email, message: 'Welcome' })
    return sent
})

عملیات را ترکیب کنید:

const createdUser = await this.User
    .create(userInfo)
    .catch(error => {
        // handle error
    })

// business logic here

return await Promise.all([
    this.pushNotification(userInfo),
    this.sendEmail(userInfo)
]).catch(error => {
    // handle errors caused
    // by pushNotification or sendEmail
})

هیچ try/catch وجود ندارد. و مشخص است که چه خطاهایی را مدیریت میکنید.