Chování klamných slibů

Většina aplikací napsaných v JS dnes používá alespoň několik volání Promises API , některé z nich používají syntaxi es5, jiné async/await . Někdy však neúplné pochopení této technologie (stejně jako jakékoli jiné) může vést k nepředvídatelnému chování, které může zmást použití a trvat hodiny, než pochopíte příčinu problému.

Strávil jsem příliš mnoho času psaním kódu JS a našel jsem zajímavý případ se sliby:sliby mají API, které může vést k nesprávné interpretaci potenciálního výsledku.

To většinou souvisí s klasickou realizací slibů es5, ale bohužel také ovlivňuje realizaci async/await slibů.

Podívejme se jako příklad na proces ukládání uživatele:

const handleSave = userData => {
  saveUser(rawUserData)
    .then(user => showNotification(`User ${getUserName(user)} has been created`))
    .catch(err => showNotification(`User was not created because of error`));
};

Tento kód vypadá snadno čitelný, ale není snadné předvídat potenciální okrajový případ. I když se snažíme být explicitní, připojili jsme náš úlovek nejen pro saveUser požadavek, ale také pro onFulfilled blok. Pokud tedy then vyvolá chybu (např. getUserName funkce vyvolá), pak bude uživatel upozorněn, že vytvoření uživatele selhalo s chybou, i když se tak stalo.

Někdo by si mohl myslet, že přepínání pořadí then/catch bloky, takže catch je připojen k saveUser zavolejte přímo. To otevírá cestu k dalšímu problému.

Použití přístupu async/wait nemusí nutně pomoci. Je agnostický ke správnému používání API a díky jeho rozsahu bloků je také jednodušší a hezčí psát jej nebezpečně, jak je uvedeno výše:

const handleSave = async userData => {
  try {
    const user = await saveUser(userData);
    showNotification(`User ${getUserName(user)} has been created`);
  } catch(error) {
    showNotification(`User was not created because of error`));
  }
};

Jak vidíte, tento kód má stejný problém jako výše.

Abychom se tomuto chování vyhnuli (při použití nativního rozhraní Promise API), musíme předat 2 zpětná volání (chybové zpětné volání, úspěšné zpětné volání) do then blok ve správném pořadí, který je hůře čitelný.

const handleSave = userData => {
  saveUser(userData)
    .then(
      user => showNotifications(`User ${getUserName(user)} has been created`),
      err => showNotifications(`User was not created because of error`));
    );
};

Aby bylo jasno, toto samo o sobě není špatné API. Ale vezmeme-li v úvahu oprávněný záměr být explicitní jako vývojář, existuje pokušení použít pojmenovanou funkci pro každou z nich, spíše než jednu then se dvěma zpětnými voláními. Odpovědný kód je méně explicitní a čitelný než nebezpečný kód – zneužití API je lákavě nebezpečné – a zároveň se cítí být explicitnější a čitelnější!

Odpovědný refaktor pomocí async/await vypadá divně. Nutnost definovat proměnné ve vyšším rozsahu mi připadá jako špatný řídicí tok. Zdá se, že pracujeme proti jazyku:

const handleSave = async userData => {
  try {
    const user = await saveUser(rawUserData)
        .catch(() => showNotifications(`User could not be saved`))

    showNotifications(`User ${displayName(user)} has been created`);
  } catch(error) {
    console.error(`User could not be saved`));
  }
};

Zatímco výše uvedené příklady jsou nebezpečné, protože by je mohli vývojáři nesprávně interpretovat, catch má být připojen ke „kořenovému“ asynchronnímu volání – existuje také nebezpečí s dlouhými řetězci myšlení, že catch je spojen s tím nejnovějším.

Například:

const createUserHandler = userData => {
  saveUser(userData)
    .then(sendWelcomeMessage)
    .catch(sendErrorMessage)
};

toto vypadá a čte se snadněji ve srovnání s odpovědným:

const createUserHandler = userData => {
  saveUser(userData)
    .then(user =>
      sendWelcomeMessage(user)
        .catch(sendErrorMessage)
    );
};

Pojďme dále, abychom viděli další způsob, jak může být rozhraní API nebezpečné:umožňuje přidat další protokolování, pokud uživatele nelze vytvořit:

const createUserHandler = userData => {
  saveUser(userData)
    .catch(logUserCreationError)
    .then(sendWelcomeEmail)
    .catch(sendErrorMessageByEmail)
};

Co chceme, je zapsat problém do našich protokolů, pokud se uložení uživatele nezdaří, ale pokud sendWelcomeMessage selhal, budeme muset odeslat chybovou zprávu na e-mail uživatele.

Protože však catch blok znovu nevyhodí ani neodmítne, vrátí vyřešený slib a tak další then blok, který volá sendWelcomeEmail se spustí, a protože neexistuje žádný uživatel, vyhodí to a my vytvoříme e-mail pro neexistujícího uživatele.

Oprava tedy vypadá ošklivě stejně jako v příkladu výše:

const createUserHandler = userData => {
  saveUser(userData)
    .then(
      logIssues,
      user =>
          sendWelcomeEmail(user)
            .catch(sendErrorMessageByEmail)
      );
};

Abychom to shrnuli, viděli jsme, jak může být API slibu pro zpracování chyb, i když zdánlivě elegantní, nebezpečné, když se vývojář posouvá směrem k čitelnosti.