ลอง / จับบล็อกด้วย async / await


119

ฉันกำลังขุดเข้าไปในโหนด 7 คุณลักษณะ async / await และยังคงสะดุดกับโค้ดเช่นนี้

function getQuote() {
  let quote = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
  return quote;
}

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch (error) {
    console.error(error);
  }
}

main();

นี่ดูเหมือนจะเป็นไปได้เพียงวิธีเดียวที่จะแก้ไข / ปฏิเสธหรือส่งคืน / โยนด้วย async / await อย่างไรก็ตาม v8 ไม่ได้ปรับโค้ดให้เหมาะสมภายในบล็อก try / catch ?!

มีทางเลือกอื่นไหม?


คำว่า 'ทิ้งหลังจากรอไม่สำเร็จ' หมายความว่าอย่างไร? หากมีข้อผิดพลาด? หากไม่ได้ผลลัพธ์ที่คาดหวัง? คุณสามารถปลูกใหม่ในบล็อกจับ
DevDig

afaik v8 เพิ่มประสิทธิภาพ try / catch คำสั่งโยนช้า
Tamas Hegedus

1
ฉันยังไม่เข้าใจคำถาม คุณแวนใช้การผูกมัดสัญญาเก่า แต่ฉันไม่คิดว่ามันจะเร็วกว่านี้ คุณกังวลเกี่ยวกับประสิทธิภาพของการลองจับหรือไม่? แล้ว async จะทำยังไง?
Tamas Hegedus

ตรวจสอบคำตอบของฉันฉันพยายามหาแนวทางที่สะอาดกว่า
zardilior

ที่นี่คุณสามารถทำstackoverflow.com/a/61833084/6482248มันดูสะอาดขึ้น
Prathamesh More

คำตอบ:


136

ทางเลือก

อีกทางเลือกหนึ่งสำหรับสิ่งนี้:

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch (error) {
    console.error(error);
  }
}

จะเป็นแบบนี้โดยใช้คำสัญญาอย่างชัดเจน:

function main() {
  getQuote().then((quote) => {
    console.log(quote);
  }).catch((error) => {
    console.error(error);
  });
}

หรืออะไรทำนองนี้โดยใช้สไตล์การส่งต่อแบบต่อเนื่อง:

function main() {
  getQuote((error, quote) => {
    if (error) {
      console.error(error);
    } else {
      console.log(quote);
    }
  });
}

ตัวอย่างต้นฉบับ

สิ่งที่รหัสเดิมของคุณทำคือระงับการดำเนินการและรอให้สัญญาที่ส่งกลับมาgetQuote()เพื่อชำระ จากนั้นดำเนินการต่อและเขียนค่าที่ส่งคืนไปvar quoteแล้วพิมพ์หากคำสัญญาได้รับการแก้ไขหรือโยนข้อยกเว้นและเรียกใช้บล็อก catch ที่พิมพ์ข้อผิดพลาดหากคำสัญญาถูกปฏิเสธ

คุณสามารถทำสิ่งเดียวกันโดยใช้ Promise API ได้โดยตรงเหมือนในตัวอย่างที่สอง

ประสิทธิภาพ

ตอนนี้สำหรับการแสดง มาทดสอบกัน!

ฉันเพิ่งเขียนรหัสนี้ - f1()ให้1เป็นค่าส่งคืนf2()โยน1เป็นข้อยกเว้น:

function f1() {
  return 1;
}

function f2() {
  throw 1;
}

ตอนนี้ขอเรียกรหัสเดียวกันล้านครั้งก่อนด้วยf1():

var sum = 0;
for (var i = 0; i < 1e6; i++) {
  try {
    sum += f1();
  } catch (e) {
    sum += e;
  }
}
console.log(sum);

จากนั้นเปลี่ยนf1()เป็นf2():

var sum = 0;
for (var i = 0; i < 1e6; i++) {
  try {
    sum += f2();
  } catch (e) {
    sum += e;
  }
}
console.log(sum);

นี่คือผลลัพธ์ที่ฉันได้รับสำหรับf1:

$ time node throw-test.js 
1000000

real    0m0.073s
user    0m0.070s
sys     0m0.004s

นี่คือสิ่งที่ฉันได้รับสำหรับf2:

$ time node throw-test.js 
1000000

real    0m0.632s
user    0m0.629s
sys     0m0.004s

ดูเหมือนว่าคุณสามารถทำบางอย่างเช่น 2 ล้านครั้งต่อวินาทีในกระบวนการเธรดเดียว หากคุณทำมากกว่านั้นคุณอาจต้องกังวลเกี่ยวกับเรื่องนี้

สรุป

ฉันจะไม่กังวลเกี่ยวกับสิ่งต่างๆเช่นนั้นในโหนด หากสิ่งต่างๆเช่นนี้ถูกนำไปใช้มากมันจะได้รับการปรับให้เหมาะสมที่สุดโดยทีม V8 หรือ SpiderMonkey หรือ Chakra และทุกคนจะปฏิบัติตาม - ไม่ใช่ว่ามันไม่ได้รับการปรับให้เหมาะสมตามหลักการมันก็ไม่ใช่ปัญหา

แม้ว่าจะไม่ได้รับการปรับให้เหมาะสมฉันก็ยังคงเถียงว่าหากคุณใช้ CPU สูงสุดใน Node คุณก็ควรเขียนจำนวนของคุณใน C - นั่นคือสิ่งที่ส่วนเสริมดั้งเดิมมีไว้สำหรับสิ่งอื่น ๆ หรือบางทีสิ่งต่างๆเช่นnode.nativeจะเหมาะกับงานมากกว่า Node.js

ฉันสงสัยว่าจะเป็นกรณีการใช้งานที่ต้องทิ้งข้อยกเว้นมากมาย โดยปกติแล้วการโยนข้อยกเว้นแทนที่จะส่งคืนค่าคือข้อยกเว้น


ฉันรู้ว่ารหัสสามารถเขียนได้อย่างง่ายดายด้วยสัญญาดังที่ได้กล่าวไว้ฉันเคยเห็นมันในตัวอย่างต่างๆนั่นคือเหตุผลที่ฉันถาม การมีการดำเนินการเดียวภายใน try / catch อาจไม่ใช่ปัญหา แต่อาจมีฟังก์ชัน async / await หลายฟังก์ชันพร้อมตรรกะของแอปพลิเคชันเพิ่มเติม
Patrick

5
@Patrick "อาจจะ" และ "จะเป็น" คือความแตกต่างระหว่างการเก็งกำไรและการทดสอบจริง ฉันทดสอบสำหรับคำสั่งเดียวเพราะนั่นคือสิ่งที่อยู่ในคำถามของคุณ แต่คุณสามารถแปลงตัวอย่างของฉันเพื่อทดสอบคำสั่งหลาย ๆ คำสั่งได้อย่างง่ายดาย ฉันยังมีตัวเลือกอื่น ๆ อีกมากมายในการเขียนโค้ดอะซิงโครนัสที่คุณถามด้วย ถ้ามันตอบคำถามของคุณแล้วคุณอาจพิจารณายอมรับคำตอบ สรุปได้: แน่นอนว่าข้อยกเว้นจะช้ากว่าผลตอบแทน แต่การใช้งานควรเป็นข้อยกเว้น
rsp

1
การโยนข้อยกเว้นควรเป็นข้อยกเว้น ดังที่กล่าวไว้รหัสจะไม่ได้รับการเพิ่มประสิทธิภาพไม่ว่าคุณจะมีข้อยกเว้นหรือไม่ก็ตาม การตีประสิทธิภาพมาจากการใช้try catchไม่ใช่จากการขว้างปาข้อยกเว้น แม้ว่าตัวเลขจะน้อย แต่ก็ช้ากว่าเกือบ 10 เท่าตามการทดสอบของคุณซึ่งไม่สำคัญ
Nepoxx

22

ทางเลือกที่คล้ายกับการจัดการข้อผิดพลาดใน Golang

เนื่องจาก async / await ใช้คำสัญญาภายใต้ประทุนคุณจึงสามารถเขียนฟังก์ชันยูทิลิตี้เล็ก ๆ น้อย ๆ ดังนี้:

export function catchEm(promise) {
  return promise.then(data => [null, data])
    .catch(err => [err]);
}

จากนั้นนำเข้าเมื่อใดก็ตามที่คุณต้องการตรวจจับข้อผิดพลาดและรวมฟังก์ชัน async ของคุณซึ่งจะส่งคืนสัญญาด้วย

import catchEm from 'utility';

async performAsyncWork() {
  const [err, data] = await catchEm(asyncFunction(arg1, arg2));
  if (err) {
    // handle errors
  } else {
    // use data
  }
}

ฉันสร้างแพ็คเกจ NPM ที่ทำตามที่กล่าวมา - npmjs.com/package/@simmo/task
Mike

2
@ ไมค์คุณอาจจะประดิษฐ์ล้อขึ้นมาใหม่ - มีแพ็คเกจยอดนิยมอยู่แล้วที่ทำแบบนี้ได้: npmjs.com/package/await-to-js
Jakub Kukul

21

อีกทางเลือกหนึ่งในการลองจับบล็อกคือawa-to-js lib ฉันมักจะใช้มัน ตัวอย่างเช่น:

import to from 'await-to-js';

async function main(callback) {
    const [err,quote] = await to(getQuote());
    
    if(err || !quote) return callback(new Error('No Quote found'));

    callback(null,quote);

}

ไวยากรณ์นี้สะอาดกว่ามากเมื่อเทียบกับ try-catch


ลองสิ่งนี้และชอบมาก ทำความสะอาดและอ่านโค้ดโดยเสียค่าใช้จ่ายในการติดตั้งโมดูลใหม่ แต่ถ้าคุณวางแผนที่จะเขียนฟังก์ชัน async จำนวนมากฉันต้องบอกว่านี่เป็นส่วนเสริมที่ยอดเยี่ยม! ขอบคุณ
filipbarak

15
async function main() {
  var getQuoteError
  var quote = await getQuote().catch(err => { getQuoteError = err }

  if (getQuoteError) return console.error(err)

  console.log(quote)
}

หรืออีกวิธีหนึ่งแทนที่จะประกาศ var ที่เป็นไปได้เพื่อระงับข้อผิดพลาดที่ด้านบนคุณสามารถทำได้

if (quote instanceof Error) {
  // ...
}

แม้ว่าจะไม่ได้ผลหากมีข้อผิดพลาดเช่น TypeError หรือ Reference ถูกโยนทิ้ง คุณสามารถมั่นใจได้ว่าเป็นข้อผิดพลาดปกติแม้ว่าจะมี

async function main() {
  var quote = await getQuote().catch(err => {
    console.error(err)      

    return new Error('Error getting quote')
  })

  if (quote instanceOf Error) return quote // get out of here or do whatever

  console.log(quote)
}

ความชอบของฉันสำหรับสิ่งนี้คือการรวมทุกอย่างไว้ในบล็อกลองจับขนาดใหญ่ที่มีการสร้างคำสัญญาหลายสัญญาทำให้ยุ่งยากในการจัดการข้อผิดพลาดเฉพาะกับสัญญาที่สร้างขึ้น ด้วยอีกทางเลือกหนึ่งคือการลองจับหลายบล็อกซึ่งฉันคิดว่ายุ่งยากพอ ๆ กัน


8

ทางเลือกที่สะอาดกว่ามีดังต่อไปนี้:

เนื่องจากความจริงที่ว่าทุกฟังก์ชัน async เป็นสัญญาทางเทคนิค

คุณสามารถเพิ่มการจับเพื่อฟังก์ชั่นเมื่อเรียกใช้โดยรอ

async function a(){
    let error;

    // log the error on the parent
    await b().catch((err)=>console.log('b.failed'))

    // change an error variable
    await c().catch((err)=>{error=true; console.log(err)})

    // return whatever you want
    return error ? d() : null;
}
a().catch(()=>console.log('main program failed'))

ไม่จำเป็นต้องลองจับเนื่องจากข้อผิดพลาดของสัญญาทั้งหมดจะได้รับการจัดการและคุณไม่มีข้อผิดพลาดของรหัสคุณสามารถละเว้นสิ่งนั้นในผู้ปกครองได้ !!

สมมติว่าคุณกำลังทำงานกับ mongodb หากมีข้อผิดพลาดคุณอาจต้องการจัดการกับฟังก์ชันที่เรียกมันมากกว่าการทำ wrapper หรือใช้การลองจับ


คุณมี 3 ฟังก์ชัน ค่าหนึ่งรับค่าและตรวจจับข้อผิดพลาดอีกค่าหนึ่งที่คุณส่งคืนหากไม่มีข้อผิดพลาดและสุดท้ายจะเรียกใช้ฟังก์ชันแรกพร้อมกับการโทรกลับเพื่อตรวจสอบว่าสิ่งนั้นส่งคืนข้อผิดพลาดหรือไม่ ทั้งหมดนี้แก้ไขได้โดย "สัญญา" เพียงครั้งเดียวจากนั้น (cb) .catch (cb) หรือบล็อก trycatch
Chief koshi

@Chiefkoshi อย่างที่คุณเห็นการจับเพียงครั้งเดียวจะไม่เกิดขึ้นเนื่องจากข้อผิดพลาดถูกจัดการแตกต่างกันในทั้งสามกรณี หากอันแรกล้มเหลวจะส่งคืน d () หากอันที่สองล้มเหลวจะส่งคืนค่า null หากอันสุดท้ายล้มเหลวข้อความแสดงข้อผิดพลาดอื่นจะแสดงขึ้น คำถามจะถามหาข้อผิดพลาดในการจัดการเมื่อใช้ await นั่นคือคำตอบเช่นกัน ทั้งหมดควรดำเนินการหากมีคนใดคนหนึ่งล้มเหลว ลองจับบล็อกจะต้องใช้สามอันในตัวอย่างนี้ซึ่งไม่สะอาดกว่า
zardilior

1
คำถามไม่ขอให้ดำเนินการหลังจากสัญญาล้มเหลว ที่นี่คุณรอ B จากนั้นเรียกใช้ C และส่งคืน D หากผิดพลาด วิธีนี้สะอาดกว่า C ต้องรอ B แต่พวกเขาไม่ได้เป็นอิสระจากกัน ฉันไม่เห็นเหตุผลว่าทำไมพวกเขาถึงอยู่ใน A ด้วยกันถ้าพวกเขาเป็นอิสระ หากพวกเขาพึ่งพาซึ่งกันและกันคุณต้องการหยุดการทำงานของ C หาก B ล้มเหลวงานของ. then.catch หรือ try-catch ฉันถือว่าพวกเขาไม่คืนค่าอะไรเลยและดำเนินการแบบอะซิงโครนัสบางอย่างที่ไม่เกี่ยวข้องกับ A. ทำไมพวกเขาจึงถูกเรียกด้วย async รอ
Chief koshi

คำถามเกี่ยวกับทางเลือกอื่นในการลองจับบล็อกเพื่อจัดการข้อผิดพลาดเมื่อใช้ async / await ตัวอย่างในที่นี้มีไว้เพื่ออธิบายและเป็นเพียงตัวอย่างเท่านั้น แสดงการจัดการการดำเนินการที่เป็นอิสระแต่ละรายการตามลำดับซึ่งโดยปกติจะใช้วิธี async / await เหตุใดพวกเขาจึงถูกเรียกด้วย async รอเพียงเพื่อแสดงให้เห็นว่าสามารถจัดการได้อย่างไร มันบรรยายมากกว่าธรรม
zardilior

2

อยากทำแบบนี้ :)

const sthError = () => Promise.reject('sth error');

const test = opts => {
  return (async () => {

    // do sth
    await sthError();
    return 'ok';

  })().catch(err => {
    console.error(err); // error will be catched there 
  });
};

test().then(ret => {
  console.log(ret);
});

คล้ายกับการจัดการข้อผิดพลาดกับ co

const test = opts => {
  return co(function*() {

    // do sth
    yield sthError();
    return 'ok';

  }).catch(err => {
    console.error(err);
  });
};

Code ไม่ใช่ผู้ชายที่ชัดเจนมากดูน่าสนใจคุณช่วยแก้ไขได้ไหม
zardilior

เป็นเรื่องโชคร้ายที่ไม่มีคำอธิบายในคำตอบนี้เพราะจริงๆแล้วมันแสดงให้เห็นถึงวิธีที่ยอดเยี่ยมในการหลีกเลี่ยงการลองจับทุก const ที่คุณกำหนดด้วยawait!
จิม

0

catchในแบบนี้จากประสบการณ์ของฉันมันอันตราย ข้อผิดพลาดใด ๆ ที่เกิดขึ้นในสแต็กทั้งหมดจะถูกจับได้ไม่ใช่แค่ข้อผิดพลาดจากสัญญานี้ (ซึ่งอาจไม่ใช่สิ่งที่คุณต้องการ)

อาร์กิวเมนต์ที่สองของคำสัญญาเป็นการเรียกกลับแบบปฏิเสธ / ล้มเหลว จะดีกว่าและปลอดภัยกว่าถ้าใช้แทน

นี่คือ typescript typesafe one-liner ที่ฉันเขียนเพื่อจัดการสิ่งนี้:

function wait<R, E>(promise: Promise<R>): [R | null, E | null] {
  return (promise.then((data: R) => [data, null], (err: E) => [null, err]) as any) as [R, E];
}

// Usage
const [currUser, currUserError] = await wait<GetCurrentUser_user, GetCurrentUser_errors>(
  apiClient.getCurrentUser()
);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.