Hướng dẫn nodejs wait for promise - nodejs chờ đợi lời hứa

Trong lập trình không đồng bộ Nodejs,

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
5 luôn là nỗi ám ảnh đối với developer trong ES5. Ví dụ như đoạn code dưới:
Hướng dẫn nodejs wait for promise - nodejs chờ đợi lời hứa

Rất may mắn đến ES6,

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
6 đã giải quyết được cơ bản
getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
5 với cấu trúc
getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
8 giúp code dễ đọc và bắt lỗi tốt hơn.

let p = new Promise(function(resolve, reject)  {  
   setTimeout(() => resolve(4),  2000);
});

// handler can't change promise, just value
p.then((res) => {
  console.log(res);
});

Nhưng đối với ứng dụng phức tạp, cần gọi nhiều xử lý không đồng bộ liên tiếp và phụ thuộc lẫn nhau thì xuất hiện

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
9 như bài toán tính tổng sau.

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)

Điều tuyệt vời nhất là ES7 xuất hiện đã giới thiệu

let getTotal = async () =>  {
  try {
    let a = await getA();
    let b = await getB(a);
    let c = await getC(b);
    return Promise.resolve(a + b + c);
  } catch (e) {
    return Promise.reject(e);
  }
}

getTotal()
.then(data => console.log(data))
.catch(err => console.log(err + ''));
0 đã cải thiện và giải quyết các vấn đề còn tồn tại của
getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
6. Bài toán tính tổng trên sẽ được viết lại như sau:

let getTotal = async () =>  {
  try {
    let a = await getA();
    let b = await getB(a);
    let c = await getC(b);
    return Promise.resolve(a + b + c);
  } catch (e) {
    return Promise.reject(e);
  }
}

getTotal()
.then(data => console.log(data))
.catch(err => console.log(err + ''));

Như ta thấy đoạn code trên rất ngắn ngọn, step rõ ràng như lập trình đồng bộ, dễ hiểu và dễ debug.

Async/await

Async/await được giới thiệu ngắn gọn như sau:

  • Async/await là cách mới để viết code bất đồng bộ. Các phương pháp làm việc với code bất đồng bộ trước đây là sử dụng callback và promise.
  • Async/await là khái niệm được xây dựng ở tầng trên promise. Do đó nó không thể sử dụng với callback thuần.
  • Async/await cũng giống như promise, là non-blocking.
  • Async/await làm cho code bất đồng bộ nhìn và chạy gần giống như code đồng bộ.

Cú pháp

Giả sử một hàm getJSON trả về một promise, promise đó chứa 1 vài đối tượng JSON. Ta cần gọi hàm đó, log các đối tượng JSON ra, sau đó trả về "done". Đoạn code sau miêu tả quá trình trên, sử dụng promise.

const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()

Còn đây là đoạn code sử dụng async/await:

const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()

Có 1 vài điểm khác biệt cần để ý:

  • Hàm có thêm từ khóa
    let getTotal = async () =>  {
      try {
        let a = await getA();
        let b = await getB(a);
        let c = await getC(b);
        return Promise.resolve(a + b + c);
      } catch (e) {
        return Promise.reject(e);
      }
    }
    
    getTotal()
    .then(data => console.log(data))
    .catch(err => console.log(err + ''));
    
    2 phía trước. Từ khóa
    let getTotal = async () =>  {
      try {
        let a = await getA();
        let b = await getB(a);
        let c = await getC(b);
        return Promise.resolve(a + b + c);
      } catch (e) {
        return Promise.reject(e);
      }
    }
    
    getTotal()
    .then(data => console.log(data))
    .catch(err => console.log(err + ''));
    
    3 chỉ được sử dụng bên trong hàm được định nghĩa với
    let getTotal = async () =>  {
      try {
        let a = await getA();
        let b = await getB(a);
        let c = await getC(b);
        return Promise.resolve(a + b + c);
      } catch (e) {
        return Promise.reject(e);
      }
    }
    
    getTotal()
    .then(data => console.log(data))
    .catch(err => console.log(err + ''));
    
    2. Bất cứ hàm
    let getTotal = async () =>  {
      try {
        let a = await getA();
        let b = await getB(a);
        let c = await getC(b);
        return Promise.resolve(a + b + c);
      } catch (e) {
        return Promise.reject(e);
      }
    }
    
    getTotal()
    .then(data => console.log(data))
    .catch(err => console.log(err + ''));
    
    2 nào cũng sẽ trả về 1 promise một cách không tường minh, và giá trị
    let getTotal = async () =>  {
      try {
        let a = await getA();
        let b = await getB(a);
        let c = await getC(b);
        return Promise.resolve(a + b + c);
      } catch (e) {
        return Promise.reject(e);
      }
    }
    
    getTotal()
    .then(data => console.log(data))
    .catch(err => console.log(err + ''));
    
    6 của promise sẽ là bất cứ cái gì mà hàm
    let getTotal = async () =>  {
      try {
        let a = await getA();
        let b = await getB(a);
        let c = await getC(b);
        return Promise.resolve(a + b + c);
      } catch (e) {
        return Promise.reject(e);
      }
    }
    
    getTotal()
    .then(data => console.log(data))
    .catch(err => console.log(err + ''));
    
    7 (trong trường hợp này là chuỗi "done").
  • Nhận xét trên cũng đồng nghĩa với việc ta không thể sử dụng
    let getTotal = async () =>  {
      try {
        let a = await getA();
        let b = await getB(a);
        let c = await getC(b);
        return Promise.resolve(a + b + c);
      } catch (e) {
        return Promise.reject(e);
      }
    }
    
    getTotal()
    .then(data => console.log(data))
    .catch(err => console.log(err + ''));
    
    3 phía trước đoạn code chứa từ khóa
    let getTotal = async () =>  {
      try {
        let a = await getA();
        let b = await getB(a);
        let c = await getC(b);
        return Promise.resolve(a + b + c);
      } catch (e) {
        return Promise.reject(e);
      }
    }
    
    getTotal()
    .then(data => console.log(data))
    .catch(err => console.log(err + ''));
    
    2.
// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
  // do something
})
  • const makeRequest = () =>
      getJSON()
        .then(data => {
          console.log(data)
          return "done"
        })
    
    makeRequest()
    
    0 có nghĩa là lời gọi
    const makeRequest = () =>
      getJSON()
        .then(data => {
          console.log(data)
          return "done"
        })
    
    makeRequest()
    
    1 sẽ chờ đến khi promise
    const makeRequest = () =>
      getJSON()
        .then(data => {
          console.log(data)
          return "done"
        })
    
    makeRequest()
    
    2 được xử lý và trả về giá trị.

Ưu điểm của Async/await là gì?

Code ngắn và sạch hơn

Đơn giản nhất chính là số lượng code ta cần viết đã giảm đi đáng kể. Trong ví dụ trên, rõ ràng rằng ta đã tiết kiệm được rất nhiều dòng code. Ta không cần viết

const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
3, tạo 1 hàm
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
4 để xử lý
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
5 hay là đặt tên data cho 1 biến ta không sử dụng. Ta tránh được các khối code lồng nhau. Những lợi ích nho nhỏ này sẽ tích tụ dần dần trong những đoạn code lớn, những project thật và sẽ trở nên rất đáng giá.

Error handling

const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
6 giúp ta xử lý cả error đồng bộ lẫn error bất đồng bộ theo cùng 1 cấu trúc. Tạm biệt
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
7. Với đoạn code dưới dùng promise,
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
7 sẽ không bắt được lỗi nếu
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
9 lỗi do nó xảy ra bên trong
const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
0. Ta cần gọi
const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
1 bên trong promise và lặp lại code xử lý error, điều mà chắc chắn sẽ trở nên rắc rối hơn cả
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
1 trong đoạn code
const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
3.

const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // this parse may fail
        const data = JSON.parse(result)
        console.log(data)
      })
      // uncomment this block to handle asynchronous errors
      // .catch((err) => {
      //   console.log(err)
      // })
  } catch (err) {
    console.log(err)
  }
}

Bây giờ hãy nhìn vào đoạn code sử dụng async/await. Khối catch giờ sẽ xử lý các lỗi parsing.

const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

Câu lệnh điều kiện

Hãy xem thử 1 đoạn code như dưới đây. Đoạn code này sẽ fetch dữ liệu và quyết định trả về giá trị hay là lấy thêm dữ liệu.

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

Đoạn code đã dần dần giống với mô hình

const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
4 mà ta thường thấy. Tổng cộng code có 6 level nested. Khi sử dụng
let getTotal = async () =>  {
  try {
    let a = await getA();
    let b = await getB(a);
    let c = await getC(b);
    return Promise.resolve(a + b + c);
  } catch (e) {
    return Promise.reject(e);
  }
}

getTotal()
.then(data => console.log(data))
.catch(err => console.log(err + ''));
0, ta sẽ có đoạn code mới dễ đọc hơn.

const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}

Giá trị intermediate

Hẳn bạn đã từng lâm vào tính huống sau: bạn cần gọi

const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
6, sau đó sử dụng giá trị nó trả về để gọi
const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
7, cuối cùng sử dụng kết quả trả về của cả 2
const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
0 trên để gọi
const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
9. Code của bạn sẽ thành ra thế này.

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
0

Nếu

const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
9 không yêu cầu tham số value1, promise sẽ bớt lớp nest đi 1 chút. Nếu bạn theo chủ nghĩa cầu toàn, bạn có thể giải quyết bằng cách wrap cả 2 giá trị value1 và value2 bằng
// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
  // do something
})
1, tránh được các lớp nest giống như đoạn code dưới.

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
1

Phương pháp này đã hi sinh tính ngữ nghĩa để đổi lấy tính dễ đọc của code. Đơn giản vì chả có lý do gì mà

// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
  // do something
})
2 và
// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
  // do something
})
3 được đặt chung vào 1 mảng, ngoại trừ việc làm như thế sẽ tránh được
const makeRequest = async () => {
  let data = await getJSON())
  console.log(data)
  return "done"
}

makeRequest()
0 bị nest. Tuy nhiên cái logic này trở nên cực kì ngớ ngẩn khi ta sử dụng
let getTotal = async () =>  {
  try {
    let a = await getA();
    let b = await getB(a);
    let c = await getC(b);
    return Promise.resolve(a + b + c);
  } catch (e) {
    return Promise.reject(e);
  }
}

getTotal()
.then(data => console.log(data))
.catch(err => console.log(err + ''));
0.

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
2

Hình dung 1 đoạn code gọi đến nhiều promise theo chuỗi. Tại 1 vị trí nào đó, đoạn code sẽ quăng ra 1 error.

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
3

Error Stack trả về từ chuỗi promise không thể giúp ta xác định error xảy ra ở đâu. Tệ hơn nữa, nó còn làm ta hiểu lầm rằng lỗi nằm ở hàm

// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
  // do something
})
6 Tuy nhiên, với
let getTotal = async () =>  {
  try {
    let a = await getA();
    let b = await getB(a);
    let c = await getC(b);
    return Promise.resolve(a + b + c);
  } catch (e) {
    return Promise.reject(e);
  }
}

getTotal()
.then(data => console.log(data))
.catch(err => console.log(err + ''));
0, Error Stack sẽ chỉ ra được hàm nào chứa lỗi.

getA().then(
  a => {
    getB(a).then(
      b => {
        getC(b).then(
          c => a + b + c,
          err => console.log(err)
        )
      },
      err => console.log(err)
    )
  },
  err => console.log(err) 
)
4

Khi bạn phát triển ứng dụng trên môi trường local, điều này thoạt nhìn không có quá nhiều tác dụng. Tuy nhiên với production server, nó lại rất hữu ích với Error Logs. Với những tình huống đó, biết được error xảy ra trong

// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
  // do something
})
8sẽ tốt hơn rất nhiều khi được báo rằng error nằm trong
// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
  // do something
})
9 phía sau
// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
  // do something
})
9 phía sau
const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // this parse may fail
        const data = JSON.parse(result)
        console.log(data)
      })
      // uncomment this block to handle asynchronous errors
      // .catch((err) => {
      //   console.log(err)
      // })
  } catch (err) {
    console.log(err)
  }
}
1

Debug

Điều tuyệt vời cuối cùng khi bạn làm việc với

let getTotal = async () =>  {
  try {
    let a = await getA();
    let b = await getB(a);
    let c = await getC(b);
    return Promise.resolve(a + b + c);
  } catch (e) {
    return Promise.reject(e);
  }
}

getTotal()
.then(data => console.log(data))
.catch(err => console.log(err + ''));
0 đó là việc debug trở nên rất đơn giản. Debug với Promise chưa bao giờ là công việc dễ chịu vì 2 lý do sau:

Kết luận

const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()
6 là 1 trong những tính năng mang tính cách mạng được thêm vào JavaScript trong vài năm gần đây. Nó giúp bạn nhận ra Promise còn thiếu sót như thế nào, cũng như cung cấp giải pháp thay thế.

Tham khảo

https://hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9