Là từ chối một Promise chỉ cho các trường hợp lỗi?


25

Hãy nói rằng tôi có chức năng này xác thực trả lại một lời hứa. Lời hứa sau đó giải quyết với kết quả. Sai và đúng là kết quả mong đợi, như tôi thấy, và sự từ chối chỉ nên xảy ra trong trường hợp lỗi. Hoặc, một thất bại trong xác thực được coi là một cái gì đó bạn sẽ từ chối một lời hứa cho?


Nếu xác thực thất bại, bạn nên rejectvà không trả về false, nhưng nếu bạn đang mong đợi giá trị là a Bool, thì bạn đã thành công và bạn nên giải quyết bằng Bool bất kể giá trị. Hứa hẹn là loại proxy cho các giá trị - chúng lưu trữ giá trị được trả về, vì vậy chỉ khi không thể lấy được giá trị reject. Nếu không thì bạn nên làm resolve.

Đây là một câu hỏi hay. Nó chạm vào một trong những thất bại của thiết kế hứa hẹn. Có hai loại lỗi, lỗi dự kiến, chẳng hạn như khi người dùng cung cấp đầu vào xấu (chẳng hạn như không đăng nhập) và lỗi không mong muốn, đó là lỗi trong mã. Thiết kế hứa hẹn hợp nhất hai khái niệm thành một luồng duy nhất khiến cho việc phân biệt hai khái niệm này trở nên khó khăn.
zzzzBov

1
Tôi muốn nói rằng giải quyết có nghĩa là sử dụng phản hồi và tiếp tục ứng dụng của bạn, trong khi từ chối có nghĩa là hủy hoạt động hiện tại (và có thể thử lại hoặc làm một cái gì đó khác).

4
một cách khác để suy nghĩ về nó - nếu đây là một cuộc gọi phương thức đồng bộ, bạn sẽ coi lỗi xác thực thông thường (tên người dùng / mật khẩu xấu) là trả lại falsehoặc ném ngoại lệ?
wrschneider

2
Các Fetch API là một ví dụ tốt về điều này. Nó luôn kích hoạt thenkhi máy chủ phản hồi - ngay cả khi mã lỗi được trả về - và bạn phải kiểm tra response.ok. Trình catchxử lý chỉ được kích hoạt cho các lỗi không mong muốn .
Mã hóa

Câu trả lời:


22

Câu hỏi hay! Không có câu trả lời khó. Nó phụ thuộc vào những gì bạn coi là đặc biệt tại điểm cụ thể của dòng chảy .

Từ chối a Promisecũng giống như đưa ra một ngoại lệ. Không phải tất cả các kết quả không mong muốn là đặc biệt , kết quả của lỗi . Bạn có thể tranh luận trường hợp của bạn theo cả hai cách:

  1. Xác thực thất bại nên rejectsự Promise, bởi vì người gọi được mong đợi một Userđối tượng trong trở lại, và bất cứ điều gì khác là một ngoại lệ đối với dòng chảy này.

  2. Xác thực thất bại nên resolvecác Promise, mặc dù đến null, vì cung cấp thông tin sai không phải là thực sự là một ngoại lệ trường hợp, và người gọi nên không mong đợi dòng chảy luôn luôn dẫn đến một User.

Lưu ý rằng tôi đang xem xét vấn đề từ phía người gọi . Trong luồng thông tin, người gọi có mong đợi hành động của mình dẫn đến một User(và bất cứ điều gì khác là lỗi) hay không, có ý nghĩa gì với người gọi cụ thể này để xử lý các kết quả khác không?

Trong một hệ thống nhiều lớp, câu trả lời có thể thay đổi khi dữ liệu chảy qua các lớp. Ví dụ:

  • Lớp HTTP nói GIẢI QUYẾT! Yêu cầu đã được gửi, ổ cắm đóng sạch và máy chủ phát ra phản hồi hợp lệ. Các Fetch API thực hiện điều này.
  • Lớp giao thức sau đó nói ĐỐI TƯỢNG! Mã trạng thái trong phản hồi là 401, phù hợp với HTTP, nhưng không phải cho giao thức!
  • Lớp xác thực nói KHÔNG, GIẢI QUYẾT! Nó bắt lỗi, vì 401 là trạng thái dự kiến ​​cho một mật khẩu sai và giải quyết cho nullngười dùng.
  • Bộ điều khiển giao diện cho biết KHÔNG CÓ THẬT, ĐỐI TƯỢNG! Phương thức hiển thị trên màn hình đang mong đợi tên người dùng và hình đại diện, và bất cứ điều gì khác ngoài thông tin đó đều là lỗi tại thời điểm này.

Ví dụ 4 điểm này rõ ràng phức tạp, nhưng nó minh họa 2 điểm:

  1. Cho dù một cái gì đó là một ngoại lệ / từ chối hay không phụ thuộc vào dòng chảy xung quanh và kỳ vọng
  2. Các lớp khác nhau trong chương trình của bạn có thể xử lý cùng một kết quả khác nhau, vì chúng nằm trong các giai đoạn khác nhau của dòng chảy

Vì vậy, một lần nữa, không có câu trả lời khó khăn. Thời gian để suy nghĩ và thiết kế!


6

Vì vậy, Promise có một đặc tính tốt là họ mang JS từ các ngôn ngữ chức năng, đó là họ thực sự triển khai hàm tạo Eitherkiểu này kết hợp hai loại khác, Leftloại và Rightloại, bằng cách buộc logic lấy một nhánh này hoặc nhánh kia chi nhánh.

data Either x y = Left x | Right y

Bây giờ bạn thực sự nhận thấy rằng loại ở phía bên trái là mơ hồ cho những lời hứa; bạn có thể từ chối với bất cứ điều gì. Điều này đúng bởi vì JS được gõ yếu, nhưng bạn muốn thận trọng nếu bạn lập trình phòng thủ.

Lý do là JS sẽ lấy các throwcâu lệnh từ mã xử lý lời hứa và gói nó vào Leftbên cạnh đó. Về mặt kỹ thuật trong JS, bạn có thể throwmọi thứ, bao gồm cả đúng / sai hoặc một chuỗi hoặc một số: nhưng mã JavaScript cũng ném mọi thứ mà không cầnthrow (khi bạn làm những việc như cố gắng truy cập các thuộc tính trên null) và có một API đã giải quyết cho điều này ( Errorđối tượng) . Vì vậy, khi bạn đi xung quanh để nắm bắt, thường rất tốt để có thể cho rằng những lỗi đó là Errorđối tượng. Và vì rejectlời hứa sẽ kết tụ trong bất kỳ lỗi nào từ bất kỳ lỗi nào ở trên, nên bạn thường chỉ muốn throwcác lỗi khác, để làm cho catchtuyên bố của bạn có logic đơn giản, nhất quán.

Do đó, mặc dù bạn có thể đặt if-condition có điều kiện catchvà tìm lỗi sai, trong trường hợp đó, trường hợp thật là không đáng kể,

Either (Either Error ()) ()

bạn có thể sẽ thích cấu trúc logic, ít nhất là cho những gì xuất hiện ngay lập tức từ trình xác thực, của một boolean đơn giản hơn:

Either Error Bool

Trong thực tế, mức logic xác thực tiếp theo có lẽ là trả về một số loại Userđối tượng có chứa người dùng được xác thực, để điều này trở thành:

Either Error (Maybe User)

và điều này ít nhiều là những gì tôi mong đợi: trả lại nulltrong trường hợp người dùng không được xác định, nếu không thì trả lại {user_id: <number>, permission_to_launch_missiles: <boolean>}. Tôi hy vọng rằng các trường hợp chung của không được đăng nhập là cứu vãn, ví dụ nếu chúng ta đang ở trong một số loại "bản demo cho các khách hàng mới" chế độ, và không nên lẫn lộn với lỗi mà tôi vô tình gọi object.doStuff()khi object.doStuffundefined.

Bây giờ với điều đó đã nói, những gì bạn có thể muốn làm là xác định một NotLoggedInhoặc PermissionErrorngoại lệ xuất phát từ Error. Sau đó, trong những điều thực sự cần nó bạn muốn viết:

function launchMissiles() {
    function actuallyLaunchThem() {
        // stub
    }
    return getAuth().then(auth => {
        if (auth === null) {
            throw new PermissionError('Cannot launch missiles without permission, cannot have permission if not logged in.');
        } else if (auth.permission_to_launch_missiles) {
            return actuallyLaunchThem();
        } else {
            throw new PermissionError(`User ${auth.user_id} does not have permission to launch the missiles.`);
        }
    });
}

3

Lỗi

Hãy nói về lỗi.

Có hai loại lỗi:

  • lỗi dự kiến
  • lỗi không mong muốn
  • lỗi do một

Lỗi dự kiến

Lỗi dự kiến ​​là trạng thái xảy ra sự cố nhưng bạn biết rằng nó có thể xảy ra, vì vậy bạn xử lý nó.

Đây là những thứ như đầu vào của người dùng hoặc yêu cầu máy chủ. Bạn biết người dùng có thể mắc lỗi hoặc máy chủ có thể bị hỏng, vì vậy bạn viết một số mã kiểm tra để đảm bảo rằng chương trình yêu cầu nhập lại hoặc hiển thị thông báo hoặc bất kỳ hành vi nào khác phù hợp.

Đây là những phục hồi khi xử lý. Nếu để lại, chúng trở thành lỗi không mong muốn.

Lỗi không mong muốn

Lỗi không mong muốn (lỗi) là trạng thái xảy ra lỗi do mã sai. Bạn biết rằng cuối cùng chúng sẽ xảy ra, nhưng không có cách nào để biết nơi nào và làm thế nào để đối phó với chúng bởi vì, theo định nghĩa, chúng là bất ngờ.

Đây là những thứ như lỗi cú pháp và logic. Bạn có thể có một lỗi đánh máy trong mã của bạn, bạn có thể đã gọi một hàm với các tham số sai. Chúng thường không thể phục hồi.

try..catch

Hãy nói về try..catch.

Trong JavaScript, throwkhông được sử dụng phổ biến. Nếu bạn nhìn xung quanh để tìm ví dụ về mã, chúng sẽ cách nhau rất xa và thường được cấu trúc dọc theo dòng

function example(param) {
  if (!Array.isArray(param) {
    throw new TypeError('"param" should be an array!');
  }
  ...
}

Do đó, try..catchcác khối không phải là tất cả những gì phổ biến cho luồng điều khiển. Thông thường khá dễ dàng để thêm một số kiểm tra trước khi gọi phương thức để tránh các lỗi dự kiến.

Các môi trường JavaScript cũng khá dễ tha thứ, vì vậy các lỗi không mong muốn cũng thường bị bỏ qua.

try..catchkhông phải là hiếm Có một số trường hợp sử dụng hay, phổ biến hơn trong các ngôn ngữ như Java và C #. Java và C # có lợi thế của các catchcấu trúc được gõ , để bạn có thể phân biệt giữa các lỗi dự kiến ​​và lỗi không mong muốn:

C # :
try
{
  var example = DoSomething();
}
catch (ExpectedException e)
{
  DoSomethingElse(e);
}

Ví dụ này cho phép các trường hợp ngoại lệ không mong muốn khác chảy lên và được xử lý ở nơi khác (chẳng hạn như bằng cách đăng nhập và đóng chương trình).

Trong JavaScript, cấu trúc này có thể được nhân rộng thông qua:

try {
  let example = doSomething();
} catch (e) {
  if (e instanceOf ExpectedError) {
    DoSomethingElse(e);
  } else {
    throw e;
  }
}

Không thanh lịch, đó là một phần lý do tại sao nó không phổ biến.

Chức năng

Hãy nói về các chức năng.

Nếu bạn sử dụng nguyên tắc trách nhiệm duy nhất , mỗi lớp và chức năng sẽ phục vụ một mục đích duy nhất.

Ví dụ authenticate()có thể xác thực người dùng.

Điều này có thể được viết là:

const user = authenticate();
if (user == null) {
  // keep doing stuff
} else {
  // handle expected error
}

Ngoài ra, nó có thể được viết là:

try {
  const user = authenticate();
  // keep doing stuff
} catch (e) {
  if (e instanceOf AuthenticationError) {
    // handle expected error
  } else {
    throw e;
  }
}

Cả hai đều được chấp nhận.

Hứa

Hãy nói về những lời hứa.

Lời hứa là một hình thức không đồng bộ của try..catch. Gọi new Promisehoặc Promise.resolvebắt đầu trymã của bạn . Gọi điện thoại throwhoặc Promise.rejectgửi cho bạn catchmã.

Promise.resolve(value)   // try
  .then(doSomething)     // try
  .then(doSomethingElse) // try
  .catch(handleError)    // catch

Nếu bạn có chức năng không đồng bộ để xác thực người dùng, bạn có thể viết nó dưới dạng:

authenticate()
  .then((user) => {
    if (user == null) {
      // keep doing stuff
    } else {
      // handle expected error
    }
  });

Ngoài ra, nó có thể được viết là:

authenticate()
  .then((user) => {
    // keep doing stuff
  })
  .catch((e) => {
    if (e instanceOf AuthenticationError) {
      // handle expected error
    } else {
      throw e;
    }
  });

Cả hai đều được chấp nhận.

Làm tổ

Hãy nói về làm tổ.

try..catchcó thể được lồng authenticate()Phương thức của bạn có thể có một try..catchkhối như:

try {
  const credentials = requestCredentialsFromUser();
  const user = getUserFromServer(credentials);
} catch (e) {
  if (e instanceOf CredentialsError) {
    // handle failure to request credentials
  } else if (e instanceOf ServerError) {
    // handle failure to get data from server
  } else {
    throw e; // no idea what happened
  }
}

Tương tự như vậy lời hứa có thể được lồng nhau. authenticate()Phương pháp không đồng bộ của bạn có thể sử dụng nội bộ lời hứa:

requestCredentialsFromUser()
  .then(getUserFromServer)
  .catch((e) => {
    if (e instanceOf CredentialsError) {
      // handle failure to request credentials
    } else if (e instanceOf ServerError) {
      // handle failure to get data from server
    } else {
      throw e; // no idea what happened
    }
  });

Vậy câu trả lời là gì?

Ok, tôi nghĩ đã đến lúc tôi thực sự trả lời câu hỏi:

Là một thất bại trong xác thực được coi là một cái gì đó bạn sẽ từ chối một lời hứa cho?

Câu trả lời đơn giản nhất tôi có thể đưa ra là bạn nên từ chối một lời hứa ở bất cứ nơi nào bạn muốn throwngoại lệ nếu đó là mã đồng bộ.

Nếu luồng kiểm soát của bạn đơn giản hơn bằng cách có một vài ifkiểm tra trong các thentuyên bố của bạn , thì không cần phải từ chối một lời hứa.

Nếu luồng điều khiển của bạn đơn giản hơn bằng cách từ chối lời hứa và sau đó kiểm tra các loại lỗi trong mã xử lý lỗi của bạn, thì hãy thực hiện thay thế.


0

Tôi đã sử dụng nhánh "từ chối" của một Promise để thể hiện hành động "hủy" của hộp thoại UI UI. Nó có vẻ tự nhiên hơn so với việc sử dụng nhánh "giải quyết", không chỉ bởi vì thường có nhiều tùy chọn "đóng" trên một hộp thoại.


Hầu hết những người theo chủ nghĩa thuần túy mà tôi biết sẽ không đồng ý với bạn.

0

Xử lý một lời hứa ít nhiều giống như điều kiện "nếu". Tùy thuộc vào việc bạn muốn "giải quyết" hay "từ chối" nếu xác thực thất bại.


1
lời hứa là không đồng bộ try..catch, không if.
zzzzBov

@zzzBox theo logic đó, bạn nên sử dụng Promise dưới dạng không đồng bộ try...catchvà nói đơn giản rằng nếu bạn có thể hoàn thành và nhận được kết quả, bạn nên giải quyết bất kể giá trị nào nhận được, nếu không bạn nên từ chối?

@ một cái gì đó, không, bạn đã hiểu sai lập luận của tôi. try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }là hoàn toàn tốt, nhưng cấu trúc mà một Promiseđại diện là try..catchmột phần, không phải là ifmột phần.
zzzzBov

@zzzzBov Tôi hiểu điều đó một cách công bằng :) Tôi thích sự tương tự. Nhưng logic của tôi chỉ đơn giản là nếu doSomething()thất bại, thì nó sẽ ném, nhưng nếu không nó có thể chứa giá trị bạn cần (phần iftrên của bạn hơi khó hiểu vì nó không phải là một phần của ý tưởng của bạn ở đây :)). Bạn chỉ nên từ chối nếu có lý do để ném (trong tương tự), vì vậy nếu thử nghiệm thất bại. Nếu thử nghiệm thành công, bạn phải luôn luôn giải quyết, bất kể giá trị của nó là dương tính, phải không?

@ một cái gì đó, tôi đã quyết định viết ra một câu trả lời (giả sử điều này vẫn mở đủ lâu), vì ý kiến ​​không đủ để bày tỏ suy nghĩ của tôi.
zzzzBov
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.