Apakah menolak Janji hanya untuk kasus kesalahan?

25

Katakanlah saya memiliki fungsi ini mengotentikasi yang mengembalikan janji. Janji itu kemudian diselesaikan dengan hasilnya. Salah dan benar adalah hasil yang diharapkan, seperti yang saya lihat, dan penolakan hanya boleh terjadi dalam kasus kesalahan. Atau, apakah kegagalan dalam otentikasi dianggap sebagai sesuatu yang Anda tolak janji?

Mathieu Bertin
sumber
Jika otentikasi gagal, Anda harus rejectdan tidak kembali palsu, tetapi jika Anda mengharapkan nilai menjadi Bool, maka Anda berada sukses dan Anda harus menyelesaikan dengan Bool terlepas dari nilai. Janji adalah semacam proxy untuk nilai - mereka menyimpan nilai yang dikembalikan, jadi hanya jika nilainya tidak dapat diperoleh jika Anda reject. Kalau tidak, Anda seharusnya resolve.
Ini pertanyaan yang bagus. Menyentuh salah satu kegagalan desain janji. Ada dua jenis kesalahan, kegagalan yang diharapkan, seperti ketika pengguna memberikan input buruk (seperti gagal masuk), dan kegagalan yang tidak terduga, yang merupakan bug dalam kode. Desain janji menggabungkan dua konsep menjadi satu aliran sehingga sulit untuk membedakan keduanya untuk penanganan.
zzzzBov
1
Saya akan mengatakan bahwa menyelesaikan berarti menggunakan respons dan melanjutkan aplikasi Anda, sementara menolak berarti membatalkan operasi saat ini (dan mungkin mencoba lagi atau melakukan sesuatu yang lain).
4
cara lain untuk memikirkannya - jika ini adalah metode panggilan yang sinkron, apakah Anda akan memperlakukan kegagalan autentikasi reguler (nama pengguna / kata sandi buruk) sebagai pengembalian falseatau sebagai melemparkan pengecualian?
wrschneider
2
The Fetch API adalah contoh yang baik dari ini. Selalu memicu thenketika server merespons - bahkan jika kode kesalahan dikembalikan - dan Anda harus memeriksa response.ok. The catchhandler hanya dipicu untuk tak terduga kesalahan.
CodingIntrigue

Jawaban:

22

Pertanyaan bagus! Tidak ada jawaban yang sulit. Itu tergantung pada apa yang Anda anggap istimewa pada titik tertentu dari aliran itu .

Menolak Promiseadalah sama dengan memunculkan pengecualian. Tidak semua hasil yang tidak diinginkan luar biasa , hasil dari kesalahan . Anda bisa memperdebatkan kasus Anda dengan dua cara:

  1. Otentikasi yang gagal harus rejectmenjadi Promise, karena pemanggil mengharapkan Userobjek sebagai imbalan, dan hal lain merupakan pengecualian untuk aliran ini.

  2. Otentikasi gagal harus resolveyang Promise, meskipun untuk null, karena memberikan kepercayaan yang salah adalah tidak benar-benar luar biasa kasus, dan penelepon tidak mengharapkan aliran untuk selalu menghasilkan User.

Perhatikan bahwa saya melihat masalah dari sisi pemanggil . Dalam arus informasi, apakah penelepon mengharapkan tindakannya menghasilkan User(dan apa pun yang salah), atau apakah masuk akal bagi penelepon khusus ini untuk menangani hasil lainnya?

Dalam sistem multi-layered, jawabannya dapat berubah saat data mengalir melalui lapisan. Sebagai contoh:

  • Lapisan HTTP mengatakan RESOLVE! Permintaan telah dikirim, soket ditutup dengan bersih, dan server mengeluarkan respons yang valid. The Fetch API melakukan hal ini.
  • Lapisan protokol kemudian mengatakan TOLAK! Kode status dalam respons adalah 401, yang ok untuk HTTP, tetapi tidak untuk protokol!
  • Lapisan otentikasi mengatakan TIDAK, RESOLVE! Itu menangkap kesalahan, karena 401 adalah status yang diharapkan untuk kata sandi yang salah, dan memutuskan untuk nullpengguna.
  • Pengontrol antarmuka mengatakan TIDAK, TOLAK! Modal yang ditampilkan di layar mengharapkan nama pengguna dan avatar, dan apa pun selain informasi itu adalah kesalahan pada saat ini.

Contoh 4 poin ini jelas rumit, tetapi menggambarkan 2 poin:

  1. Apakah sesuatu merupakan pengecualian / penolakan atau tidak tergantung pada arus dan harapan di sekitarnya
  2. Lapisan yang berbeda dari program Anda mungkin memperlakukan hasil yang sama secara berbeda, karena mereka berada di berbagai tahap aliran

Jadi sekali lagi, tidak ada jawaban yang sulit. Saatnya berpikir dan mendesain!

slezica
sumber
6

Jadi Janji memiliki properti yang bagus yang mereka bawa JS dari bahasa fungsional, yaitu bahwa mereka memang mengimplementasikan Eitherkonstruktor tipe ini yang merekatkan dua tipe lainnya, Lefttipe dan Righttipe, dengan memaksa logika untuk mengambil satu cabang atau yang lain cabang.

data Either x y = Left x | Right y

Sekarang Anda benar-benar memperhatikan bahwa tipe di sisi kiri tidak jelas untuk janji; Anda bisa menolak dengan apa pun. Ini benar karena JS diketik dengan lemah, tetapi Anda ingin berhati-hati jika Anda memprogram pertahanan.

Alasannya adalah bahwa JS akan mengambil throwpernyataan dari kode penanganan janji dan menggabungkannya ke Leftsisi itu juga. Secara teknis di JS Anda dapat throwsegalanya, termasuk true / false atau string atau angka: tetapi kode JavaScript juga melempar sesuatu tanpathrow (ketika Anda melakukan hal-hal seperti mencoba mengakses properti pada nulls) dan ada API menetap untuk ini ( Errorobjek) . Jadi ketika Anda berkeliling untuk menangkap, biasanya baik untuk dapat menganggap bahwa kesalahan itu adalah Errorobjek. Dan karena rejectuntuk janji akan menggumpal dalam kesalahan dari salah satu bug di atas, Anda biasanya hanya ingin throwkesalahan lain, untuk membuat catchpernyataan Anda memiliki logika yang sederhana dan konsisten.

Oleh karena itu meskipun Anda dapat menempatkan jika-kondisional di Anda catchdan mencari kesalahan palsu, dalam hal ini kasus kebenaran itu sepele,

Either (Either Error ()) ()

Anda mungkin akan lebih suka struktur logika, setidaknya untuk apa yang keluar langsung dari autentikator, dari boolean yang lebih sederhana:

Either Error Bool

Bahkan level berikutnya dari logika otentikasi mungkin adalah untuk mengembalikan beberapa jenis Userobjek yang berisi pengguna yang diautentikasi, sehingga ini menjadi:

Either Error (Maybe User)

dan ini kurang lebih seperti yang saya harapkan: return nulldalam kasus di mana pengguna tidak didefinisikan, jika tidak kembali {user_id: <number>, permission_to_launch_missiles: <boolean>}. Aku berharap bahwa kasus umum dari tidak sedang login adalah diselamatkan, misalnya jika kita berada di semacam "demo untuk klien baru" mode, dan tidak boleh dicampur dengan bug di mana saya sengaja dipanggil object.doStuff()saat object.doStuffitu undefined.

Sekarang dengan mengatakan, apa yang mungkin ingin Anda lakukan adalah mendefinisikan NotLoggedInatauPermissionError pengecualian yang berasal dari Error. Kemudian dalam hal-hal yang benar-benar membutuhkannya Anda ingin menulis:

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.`);
        }
    });
}
CR Drost
sumber
3

Kesalahan

Mari kita bicara tentang kesalahan.

Ada dua jenis kesalahan:

  • kesalahan yang diharapkan
  • kesalahan tak terduga
  • kesalahan satu per satu

Kesalahan yang Diharapkan

Kesalahan yang diharapkan adalah keadaan di mana hal yang salah terjadi tetapi Anda tahu bahwa itu mungkin terjadi, jadi Anda menanganinya.

Ini adalah hal-hal seperti input pengguna atau permintaan server. Anda tahu pengguna mungkin membuat kesalahan atau bahwa server mungkin turun, jadi Anda menulis beberapa kode pemeriksaan untuk memastikan bahwa program meminta input lagi, atau menampilkan pesan, atau apa pun perilaku lain yang sesuai.

Ini dapat dipulihkan saat ditangani. Jika dibiarkan, mereka menjadi kesalahan yang tidak terduga.

Kesalahan Tidak Terduga

Kesalahan tak terduga (bug) adalah keadaan di mana hal yang salah terjadi karena kode salah. Anda tahu bahwa mereka pada akhirnya akan terjadi, tetapi tidak ada cara untuk mengetahui di mana atau bagaimana menghadapinya karena, menurut definisi, mereka tidak terduga.

Ini adalah hal-hal seperti kesalahan sintaksis dan logika. Anda mungkin memiliki kesalahan ketik dalam kode Anda, Anda mungkin telah memanggil fungsi dengan parameter yang salah. Ini biasanya tidak dapat dipulihkan.

try..catch

Mari kita bicarakan try..catch.

Dalam JavaScript, throwtidak umum digunakan. Jika Anda mencari-cari contoh dalam kode mereka akan sedikit dan jauh di antara, dan biasanya terstruktur di sepanjang baris

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

Karena itu, try..catchblok tidak terlalu umum untuk aliran kontrol. Biasanya cukup mudah untuk menambahkan beberapa cek sebelum memanggil metode untuk menghindari kesalahan yang diharapkan.

Lingkungan JavaScript juga cukup memaafkan, sehingga kesalahan yang tak terduga seringkali juga diabaikan.

try..catchtidak harus biasa. Ada beberapa kasus penggunaan yang bagus, yang lebih umum dalam bahasa seperti Java dan C #. Java dan C # memiliki keunggulan mengetikcatch konstruksi yang , sehingga Anda dapat membedakan antara kesalahan yang diharapkan dan yang tidak terduga:

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

Contoh ini memungkinkan pengecualian tak terduga lainnya mengalir dan ditangani di tempat lain (seperti dengan cara login dan menutup program).

Dalam JavaScript, konstruk ini dapat direplikasi melalui:

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

Bukan sebagai elegan, yang merupakan bagian dari alasan mengapa itu tidak biasa.

Fungsi

Mari kita bicara tentang fungsi.

Jika Anda menggunakan prinsip tanggung jawab tunggal , setiap kelas dan fungsi harus melayani tujuan tunggal.

Sebagai contoh authenticate() dapat mengotentikasi pengguna.

Ini dapat ditulis sebagai:

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

Atau dapat juga ditulis sebagai:

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

Keduanya bisa diterima.

Janji

Mari kita bicara tentang janji.

Janji adalah bentuk asinkron dari try..catch. Memanggil new Promiseatau Promise.resolvememulai trykode Anda . Memanggil throwatau Promise.rejectmengirim Anda ke catchkode.

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

Jika Anda memiliki fungsi asinkron untuk mengautentikasi pengguna, Anda dapat menuliskannya sebagai:

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

Atau dapat juga ditulis sebagai:

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

Keduanya bisa diterima.

Bersarang

Mari kita bicara tentang bersarang.

try..catchbisa disarangkan. authenticate()Metode Anda mungkin secara internal memiliki try..catchblok seperti:

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
  }
}

Demikian juga janji bisa disarangkan. authenticate()Metode async Anda mungkin menggunakan janji secara internal:

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
    }
  });

Jadi apa jawabannya?

Ok, saya pikir sudah waktunya bagi saya untuk benar-benar menjawab pertanyaan:

Apakah kegagalan dalam otentikasi dianggap sebagai sesuatu yang Anda tolak janji?

Jawaban paling sederhana yang bisa saya berikan adalah bahwa Anda harus menolak janji di mana pun Anda ingin throw pengecualian jika itu kode sinkron.

Jika aliran kontrol Anda lebih sederhana dengan melakukan beberapa ifpemeriksaan dalam thenlaporan Anda , tidak perlu menolak janji.

Jika aliran kontrol Anda lebih sederhana dengan menolak janji dan kemudian memeriksa jenis kesalahan dalam kode penanganan kesalahan Anda, maka lakukan itu.

zzzzBov
sumber
0

Saya telah menggunakan cabang "tolak" dari Janji untuk mewakili aksi "batal" dari kotak dialog jQuery UI. Tampaknya lebih alami daripada menggunakan cabang "menyelesaikan", paling tidak karena sering ada beberapa opsi "tutup" pada kotak dialog.

Alnitak
sumber
Kebanyakan puritan yang saya tahu tidak akan setuju dengan Anda.
0

Menangani janji kurang lebih seperti kondisi "jika". Terserah Anda apakah Anda ingin "menyelesaikan" atau "menolak" jika otentikasi gagal.

evilReiko
sumber
1
janji itu asinkron try..catch, bukan if.
zzzzBov
@zzzBox jadi dengan logika itu, Anda harus menggunakan Janji sebagai asinkron try...catchdan hanya mengatakan bahwa jika Anda dapat menyelesaikan dan mendapatkan hasil, Anda harus menyelesaikan terlepas dari nilai yang diterima, jika tidak, Anda harus menolak?
@ entah di mana, tidak, Anda salah mengartikan argumen saya. try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }baik-baik saja, tetapi konstruksi yang Promisemewakili adalah try..catchbagian, bukan ifbagian.
zzzzBov
@zzzzBov Saya mendapatkan itu dalam semua keadilan :) Saya suka analoginya. Tapi logika saya adalah bahwa jika doSomething()gagal, maka itu akan melempar, tetapi jika tidak bisa mengandung nilai yang Anda butuhkan (di ifatas Anda sedikit membingungkan karena itu bukan bagian dari ide Anda di sini :)). Anda hanya boleh menolak jika ada alasan untuk melempar (dalam analogi), jadi jika tes gagal. Jika tes berhasil, Anda harus selalu menyelesaikannya, terlepas dari apakah nilainya positif, bukan?
@ entah di mana, saya sudah memutuskan untuk menulis jawaban (dengan asumsi ini tetap terbuka cukup lama), karena komentar tidak cukup untuk mengekspresikan pikiran saya.
zzzzBov