Name already in use
ru.javascript.info / 1-js / 11-async / 04-promise-error-handling / article.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Промисы: обработка ошибок
Цепочки промисов отлично подходят для перехвата ошибок. Если промис завершается с ошибкой, то управление переходит в ближайший обработчик ошибок. На практике это очень удобно.
Например, в представленном ниже примере для fetch указана неправильная ссылка (сайт не существует), и .catch перехватывает ошибку:
Как видно, .catch не обязательно должен быть сразу после ошибки, он может быть далее, после одного или даже нескольких .then
Или, может быть, с сервером всё в порядке, но в ответе мы получим некорректный JSON. Самый лёгкий путь перехватить все ошибки — это добавить .catch в конец цепочки:
Если все в порядке, то такой .catch вообще не выполнится. Но если любой из промисов будет отклонён (проблемы с сетью или некорректная json-строка, или что угодно другое), то ошибка будет перехвачена.
Вокруг функции промиса и обработчиков находится «невидимый try..catch «. Если происходит исключение, то оно перехватывается, и промис считается отклонённым с этой ошибкой.
Например, этот код:
. Работает так же, как и этот:
«Невидимый try..catch » вокруг промиса автоматически перехватывает ошибку и превращает её в отклонённый промис.
Это работает не только в функции промиса, но и в обработчиках. Если мы бросим ошибку ( throw ) из обработчика ( .then ), то промис будет считаться отклонённым, и управление перейдёт к ближайшему обработчику ошибок.
Это происходит для всех ошибок, не только для тех, которые вызваны оператором throw . Например, программная ошибка:
Финальный .catch перехватывает как промисы, в которых вызван reject , так и случайные ошибки в обработчиках.
Как мы уже заметили, .catch ведёт себя как try..catch . Мы можем иметь столько обработчиков .then , сколько мы хотим, и затем использовать один .catch в конце, чтобы перехватить ошибки из всех обработчиков.
В обычном try..catch мы можем проанализировать ошибку и повторно пробросить дальше, если не можем её обработать. То же самое возможно для промисов.
Если мы пробросим ( throw ) ошибку внутри блока .catch , то управление перейдёт к следующему ближайшему обработчику ошибок. А если мы обработаем ошибку и завершим работу обработчика нормально, то продолжит работу ближайший успешный обработчик .then .
В примере ниже .catch успешно обрабатывает ошибку:
Здесь блок .catch завершается нормально. Поэтому вызывается следующий успешный обработчик .then .
В примере ниже мы видим другую ситуацию с блоком .catch . Обработчик (*) перехватывает ошибку и не может обработать её (например, он знает как обработать только URIError ), поэтому ошибка пробрасывается далее:
Управление переходит от первого блока .catch (*) к следующему (**) , вниз по цепочке.
Что произойдёт, если ошибка не будет обработана? Например, мы просто забыли добавить .catch в конец цепочки, как здесь:
В случае ошибки выполнение должно перейти к ближайшему обработчику ошибок. Но в примере выше нет никакого обработчика. Поэтому ошибка как бы «застревает», её некому обработать.
На практике, как и при обычных необработанных ошибках в коде, это означает, что что-то пошло сильно не так.
Что происходит, когда обычная ошибка не перехвачена try..catch ? Скрипт умирает с сообщением в консоли. Похожее происходит и в случае необработанной ошибки промиса.
JavaScript-движок отслеживает такие ситуации и генерирует в этом случае глобальную ошибку. Вы можете увидеть её в консоли, если запустите пример выше.
В браузере мы можем поймать такие ошибки, используя событие unhandledrejection :
Это событие является частью стандарта HTML.
Если происходит ошибка, и отсутствует её обработчик, то генерируется событие unhandledrejection , и соответствующий объект event содержит информацию об ошибке.
Обычно такие ошибки неустранимы, поэтому лучше всего — информировать пользователя о проблеме и, возможно, отправить информацию об ошибке на сервер.
Js почему вс код подчеркивает необработанный промис
Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler. That’s very convenient in practice.
For instance, in the code below the URL to fetch is wrong (no such site) and .catch handles the error:
As you can see, the .catch doesn’t have to be immediate. It may appear after one or maybe several .then .
Or, maybe, everything is all right with the site, but the response is not valid JSON. The easiest way to catch all errors is to append .catch to the end of chain:
Normally, such .catch doesn’t trigger at all. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it.
Implicit try…catch
The code of a promise executor and promise handlers has an «invisible try..catch » around it. If an exception happens, it gets caught and treated as a rejection.
For instance, this code:
…Works exactly the same as this:
The «invisible try..catch » around the executor automatically catches the error and turns it into rejected promise.
This happens not only in the executor function, but in its handlers as well. If we throw inside a .then handler, that means a rejected promise, so the control jumps to the nearest error handler.
Here’s an example:
This happens for all errors, not just those caused by the throw statement. For example, a programming error:
The final .catch not only catches explicit rejections, but also accidental errors in the handlers above.
Rethrowing
As we already noticed, .catch at the end of the chain is similar to try..catch . We may have as many .then handlers as we want, and then use a single .catch at the end to handle errors in all of them.
In a regular try..catch we can analyze the error and maybe rethrow it if it can’t be handled. The same thing is possible for promises.
If we throw inside .catch , then the control goes to the next closest error handler. And if we handle the error and finish normally, then it continues to the next closest successful .then handler.
In the example below the .catch successfully handles the error:
Here the .catch block finishes normally. So the next successful .then handler is called.
In the example below we see the other situation with .catch . The handler (*) catches the error and just can’t handle it (e.g. it only knows how to handle URIError ), so it throws it again:
The execution jumps from the first .catch (*) to the next one (**) down the chain.
Unhandled rejections
What happens when an error is not handled? For instance, we forgot to append .catch to the end of the chain, like here:
In case of an error, the promise becomes rejected, and the execution should jump to the closest rejection handler. But there is none. So the error gets “stuck”. There’s no code to handle it.
In practice, just like with regular unhandled errors in code, it means that something has gone terribly wrong.
What happens when a regular error occurs and is not caught by try..catch ? The script dies with a message in the console. A similar thing happens with unhandled promise rejections.
The JavaScript engine tracks such rejections and generates a global error in that case. You can see it in the console if you run the example above.
In the browser we can catch such errors using the event unhandledrejection :
The event is the part of the HTML standard.
If an error occurs, and there’s no .catch , the unhandledrejection handler triggers, and gets the event object with the information about the error, so we can do something.
Usually such errors are unrecoverable, so our best way out is to inform the user about the problem and probably report the incident to the server.
Поиск проблемных промисов в JavaScript
▍Краткий рассказ о JavaScript-промисах
- Вершины-промисы (p) представляют собой случаи запуска промисов.
- Вершины-значения (v) представляют значения, с которыми разрешаются или отклоняются промисы. Это могут быть функции.
- Вершины-функции (f) — это функции, зарегистрированные для обработки разрешения или отклонения промиса.
- Вершины-механизмы синхронизации (s) представляют собой все использованные в коде вызовы Promise.all() или Promise.race() .
- Рёбра разрешения или отклонения промиса (v)->(p) указывают на связи вершин-значений с вершинами-промисами. Они помечены как resolve или reject .
- Рёбра регистрации обработчиков (p)->(f) указывают на связи между промисом и функцией. Они помечены как onResolve или onReject .
- Рёбра связей (p1)->(p2) показывают взаимоотношения между связанными промисами.
- Рёбра return или throw (f)->(v) показывают связи функций и значений. Они помечены как return или throw .
- Рёбра механизмов синхронизации указывают на связи, идущие от множества промисов к одному механизму синхронизации промисов. Они помечаются как resolved , rejected или pending на основании того, как ведёт себя новый промис.
▍Использование PromiseKeeper для поиска анти-паттернов
- Пропущенные обработчики отклонения промисов — это ведёт к «проглатыванию» ошибок.
- Попытки многократного завершения работы промисов — это происходит, когда пытаются разрешить или отклонить промис, работа которого уже была завершена.
- Незавершённые промисы, то есть такие, которые не разрешены, но и не отклонены в то время, когда PromiseKeeper строит граф.
- Недостижимые обработчики — то есть код, зарегистрированный для обработки разрешения или отклонения промисов, который не выполняется во время динамического анализа кода, выполняемого PromiseKeeper.
- Неявные возвраты и стандартные обработчики — это может привести к неожиданному поведению промисов, расположенных ниже в цепочке промисов.
- Ненужные промисы — когда намеренно создают новый промис в функции, которая уже обёрнута в промис.
Using Promises
A Promise is an object representing the eventual completion or failure of an asynchronous operation. Since most people are consumers of already-created promises, this guide will explain consumption of returned promises before explaining how to create them.
Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.
Imagine a function, createAudioFileAsync() , which asynchronously generates a sound file given a configuration record and two callback functions, one called if the audio file is successfully created, and the other called if an error occurs.
Here’s some code that uses createAudioFileAsync() :
If createAudioFileAsync() were rewritten to return a promise, you would attach your callbacks to it instead:
This convention has several advantages. We will explore each one.
Guarantees
Unlike old-fashioned passed-in callbacks, a promise comes with some guarantees:
- Callbacks added with then() will never be invoked before the completion of the current run of the JavaScript event loop.
- These callbacks will be invoked even if they were added after the success or failure of the asynchronous operation that the promise represents.
- Multiple callbacks may be added by calling then() several times. They will be invoked one after another, in the order in which they were inserted.
One of the great things about using promises is chaining.
Chaining
A common need is to execute two or more asynchronous operations back to back, where each subsequent operation starts when the previous operation succeeds, with the result from the previous step. We accomplish this by creating a promise chain.
Here’s the magic: the then() function returns a new promise, different from the original:
This second promise ( promise2 ) represents the completion not just of doSomething() , but also of the successCallback or failureCallback you passed in, which can be other asynchronous functions returning a promise. When that’s the case, any callbacks added to promise2 get queued behind the promise returned by either successCallback or failureCallback .
Basically, each promise represents the completion of another asynchronous step in the chain.
In the old days, doing several asynchronous operations in a row would lead to the classic callback pyramid of doom:
With modern functions, we attach our callbacks to the returned promises instead, forming a promise chain:
The arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback) . You might see this expressed with arrow functions instead:
Important: Always return results, otherwise callbacks won’t catch the result of a previous promise (with arrow functions, () => x is short for () => ). If the previous handler started a promise but did not return it, there’s no way to track its settlement anymore, and the promise is said to be «floating».
This may be worse if you have race conditions — if the promise from the last handler is not returned, the next then handler will be called early, and any value it reads may be incomplete.
Therefore, as a rule of thumb, whenever your operation encounters a promise, return it and defer its handling to the next then handler.
Chaining after a catch
It’s possible to chain after a failure, i.e. a catch , which is useful to accomplish new actions even after an action failed in the chain. Read the following example:
This will output the following text:
Note: The text «Do this» is not displayed because the «Something failed» error caused a rejection.
Error propagation
You might recall seeing failureCallback three times in the pyramid of doom earlier, compared to only once at the end of the promise chain:
If there’s an exception, the browser will look down the chain for .catch() handlers or onRejected . This is very much modeled after how synchronous code works:
This symmetry with asynchronous code culminates in the async / await syntax:
It builds on promises, e.g. doSomething() is the same function as before. You can read more about the syntax here.
Promises solve a fundamental flaw with the callback pyramid of doom, by catching all errors, even thrown exceptions and programming errors. This is essential for functional composition of asynchronous operations.
Promise rejection events
Whenever a promise is rejected, one of two events is sent to the global scope (generally, this is either the window or, if being used in a web worker, it’s the Worker or other worker-based interface). The two events are:
Sent when a promise is rejected, after that rejection has been handled by the executor’s reject function.
Sent when a promise is rejected but there is no rejection handler available.
In both cases, the event (of type PromiseRejectionEvent ) has as members a promise property indicating the promise that was rejected, and a reason property that provides the reason given for the promise to be rejected.
These make it possible to offer fallback error handling for promises, as well as to help debug issues with your promise management. These handlers are global per context, so all errors will go to the same event handlers, regardless of source.
One case of special usefulness: when writing code for Node.js, it’s common that modules you include in your project may have unhandled rejected promises, logged to the console by the Node.js runtime. You can capture them for analysis and handling by your code—or just to avoid having them cluttering up your output—by adding a handler for the Node.js unhandledRejection event (notice the difference in capitalization of the name), like this:
For Node.js, to prevent the error from being logged to the console (the default action that would otherwise occur), adding that process.on() listener is all that’s necessary; there’s no need for an equivalent of the browser runtime’s preventDefault() method.
However, if you add that process.on listener but don’t also have code within it to handle rejected promises, they will just be dropped on the floor and silently ignored. So ideally, you should add code within that listener to examine each rejected promise and make sure it was not caused by an actual code bug.
Creating a Promise around an old callback API
A Promise can be created from scratch using its constructor. This should be needed only to wrap old APIs.
In an ideal world, all asynchronous functions would already return promises. Unfortunately, some APIs still expect success and/or failure callbacks to be passed in the old way. The most obvious example is the setTimeout() function:
Mixing old-style callbacks and promises is problematic. If saySomething() fails or contains a programming error, nothing catches it. setTimeout is to blame for this.
Luckily we can wrap setTimeout in a promise. Best practice is to wrap problematic functions at the lowest possible level, and then never call them directly again:
Basically, the promise constructor takes an executor function that lets us resolve or reject a promise manually. Since setTimeout() doesn’t really fail, we left out reject in this case.
Composition
Promise.resolve() and Promise.reject() are shortcuts to manually create an already resolved or rejected promise respectively. This can be useful at times.
Promise.all() and Promise.race() are two composition tools for running asynchronous operations in parallel.
We can start operations in parallel and wait for them all to finish like this:
It is important to note that if one of the promises in the array rejects, Promise.all() will throw the error and abort the other operations. This may cause unexpected state or behavior. Promise.allSettled() is another composition tool that ensures all operations are complete before resolving.
Sequential composition is possible using some clever JavaScript:
Basically, we reduce an array of asynchronous functions down to a promise chain. The code above is equivalent to:
This can be made into a reusable compose function, which is common in functional programming:
The composeAsync() function will accept any number of functions as arguments, and will return a new function that accepts an initial value to be passed through the composition pipeline:
Sequential composition can also be done more succinctly with async/await:
Timing
To avoid surprises, functions passed to then() will never be called synchronously, even with an already-resolved promise:
Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later (only after the function which created it exits, and when the JavaScript execution stack is empty), just before control is returned to the event loop; i.e. pretty soon:
Task queues vs microtasks
Promise callbacks are handled as a Microtask whereas setTimeout() callbacks are handled as Task queues.
The code above will output:
For more details, refer to Tasks vs microtasks.
Nesting
Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition. See common mistakes.
Nesting is a control structure to limit the scope of catch statements. Specifically, a nested catch only catches failures in its scope and below, not errors higher up in the chain outside the nested scope. When used correctly, this gives greater precision in error recovery:
Note that the optional steps here are nested, not from the indentation, but from the precarious placement of the outer ( and ) around them.
The inner neutralizing catch statement only catches failures from doSomethingOptional() and doSomethingExtraNice() , after which the code resumes with moreCriticalStuff() . Importantly, if doSomethingCritical() fails, its error is caught by the final (outer) catch only.
Common mistakes
Here are some common mistakes to watch out for when composing promise chains. Several of these mistakes manifest in the following example:
The first mistake is to not chain things together properly. This happens when we create a new promise but forget to return it. As a consequence, the chain is broken, or rather, we have two independent chains racing. This means doFourthThing() won’t wait for doSomethingElse() or doThirdThing() to finish, and will run in parallel with them, likely unintended. Separate chains also have separate error handling, leading to uncaught errors.
The second mistake is to nest unnecessarily, enabling the first mistake. Nesting also limits the scope of inner error handlers, which—if unintended—can lead to uncaught errors. A variant of this is the promise constructor anti-pattern, which combines nesting with redundant use of the promise constructor to wrap code that already uses promises.
The third mistake is forgetting to terminate chains with catch . Unterminated promise chains lead to uncaught promise rejections in most browsers.
A good rule-of-thumb is to always either return or terminate promise chains, and as soon as you get a new promise, return it immediately, to flatten things:
Note that () => x is short for () => .
Now we have a single deterministic chain with proper error handling.
Using async / await addresses most, if not all of these problems—the tradeoff being that the most common mistake with that syntax is forgetting the await keyword.
When promises and tasks collide
If you run into situations in which you have promises and tasks (such as events or callbacks) which are firing in unpredictable orders, it’s possible you may benefit from using a microtask to check status or balance out your promises when promises are created conditionally.
If you think microtasks may help solve this problem, see the microtask guide to learn more about how to use queueMicrotask() to enqueue a function as a microtask.
Error handling with promises
Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler. That’s very convenient in practice.
For instance, in the code below the URL to fetch is wrong (no such site) and .catch handles the error:
As you can see, the .catch doesn’t have to be immediate. It may appear after one or maybe several .then .
Or, maybe, everything is all right with the site, but the response is not valid JSON. The easiest way to catch all errors is to append .catch to the end of chain:
Normally, such .catch doesn’t trigger at all. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it.
Implicit try…catch
The code of a promise executor and promise handlers has an "invisible try..catch " around it. If an exception happens, it gets caught and treated as a rejection.
For instance, this code:
…Works exactly the same as this:
The "invisible try..catch " around the executor automatically catches the error and turns it into rejected promise.
This happens not only in the executor function, but in its handlers as well. If we throw inside a .then handler, that means a rejected promise, so the control jumps to the nearest error handler.
Here’s an example:
This happens for all errors, not just those caused by the throw statement. For example, a programming error:
The final .catch not only catches explicit rejections, but also accidental errors in the handlers above.
Rethrowing
As we already noticed, .catch at the end of the chain is similar to try..catch . We may have as many .then handlers as we want, and then use a single .catch at the end to handle errors in all of them.
In a regular try..catch we can analyze the error and maybe rethrow it if it can’t be handled. The same thing is possible for promises.
If we throw inside .catch , then the control goes to the next closest error handler. And if we handle the error and finish normally, then it continues to the next closest successful .then handler.
In the example below the .catch successfully handles the error:
Here the .catch block finishes normally. So the next successful .then handler is called.
In the example below we see the other situation with .catch . The handler (*) catches the error and just can’t handle it (e.g. it only knows how to handle URIError ), so it throws it again:
The execution jumps from the first .catch (*) to the next one (**) down the chain.
Unhandled rejections
What happens when an error is not handled? For instance, we forgot to append .catch to the end of the chain, like here:
In case of an error, the promise becomes rejected, and the execution should jump to the closest rejection handler. But there is none. So the error gets “stuck”. There’s no code to handle it.
In practice, just like with regular unhandled errors in code, it means that something has gone terribly wrong.
What happens when a regular error occurs and is not caught by try..catch ? The script dies with a message in the console. A similar thing happens with unhandled promise rejections.
The JavaScript engine tracks such rejections and generates a global error in that case. You can see it in the console if you run the example above.
In the browser we can catch such errors using the event unhandledrejection :
The event is the part of the HTML standard.
If an error occurs, and there’s no .catch , the unhandledrejection handler triggers, and gets the event object with the information about the error, so we can do something.
Usually such errors are unrecoverable, so our best way out is to inform the user about the problem and probably report the incident to the server.
Ошибка Uncaught (in promise)
Распознавание ошибок — обычное дело для программистов и веб-мастеров, самостоятельно прописывающих программные коды для своих ресурсов. Но порой с сообщением Error сталкиваются обычные пользователи, не имеющие даже приблизительного понятия о том, как быть в сложившейся ситуации. И возникает ступор: что делать? То ли свои действия исправлять, то ли вызывать специалистов, то ли технику уже на свалку везти?
Ошибка Uncaught (in promise) может появляться при работе с JavaScript, браузерами, приложениями и даже мессенджерами. Для понимания причины значение имеет характеристика конкретного сбоя, выводимая после Error.
В любом случае, это ошибка, возникшая в кодировке асинхронной операции с использованием функции обратного вызова. Снижая производительность кода до нуля, формально она не является критичной и, при распознавании, легко устраняется. Однако диагностировать причину бывает крайне трудоемко — именно на это указывает термин Uncaught (“необработанная”). И разобраться с возникающей проблемой по силам только грамотному и внимательному мастеру. Новичку же лучше обратиться за подсказкой к более опытным коллегам.
Для решения проблемы разработчику, как правило, предстоит вручную перебирать все варианты, которые способны вызвать несоответствие и сбой, устраняя конфликт между поставленной задачей и возможностью ее исполнения.
На практике промисы создаются как раз для того, чтобы перехватить ошибку, понять, на каком именно участке кода она возникает. Если бы не использовался Promise, команда просто оставалась бы без исполнения, а пользователь пребывал в неведении: почему та или иная функция не срабатывает? Очевидно, что информирование существенно упрощает задачу и сокращает время на реставрацию процесса.
При определении ошибки, перебирая цепочки, находят функцию, нарушающую логику. Ее обработка приводит к восстановлению работы системы. По сути, promise — это объект, не идущий в систему, а возвращающий в исходную точку сообщение о том, насколько удачно прошла операция. Если же произошел сбой — то какова его вероятная причина.
В то же время многократное использование промисов, связанное с ожиданием ответа, замедляет работу ресурса, утяжеляет его. Поэтому злоупотреблять удобной конструкцией не следует, как бы ни хотелось. Решение заключается в том, чтобы выбрать наиболее важный (или наиболее уязвимый) связующий блок, и именно на него установить необходимый код промис.
Работа по диагностике и обработке ошибок
Собственно, работа по диагностике и обработке ошибок, возникающих при создании сайтов, приложений или даже текстов в редакторе, почти всегда идет по тому же алгоритму: простым перебором исключаешь ошибочные варианты, один за другим.
Наглядный тому пример — поиск ответа на вопрос, почему перестали работать сочетания клавиш Ctrl+C и Ctrl+V. Комбинации “горячих клавиш” могут отказаться реагировать на нажатие из-за заражения вирусом, загрязнения клавиатуры, целого ряда других факторов. Тестируя их один за другим, раньше или позже непременно выйдешь на истинную причину — и тогда уже сможешь ее устранить.
Аналогично необходимо действовать при возникновении ошибки Uncaught (in promise): проверяя строчку за строчкой, каждый показатель, находить неверное значение, направляющее не туда, куда планировалось, переписывать его и добиваться намеченной цели.
Если вы не являетесь разработчиком, не создаете авторских приложений, не владеете профессиональным языком и никогда не касались даже близко программных кодов, но столкнулись с ошибкой Uncaught (in promise), прежде всего проверьте технику на присутствие вирусов. Запущенный злоумышленниками вредитель мог покалечить вашу систему в своих интересах. Второй шаг — попытка восстановления системы в точке, где она благополучно работала ранее. Вспомните, какие обновления вы устанавливали в последнюю очередь, попытайтесь избавиться от них. Если речь не идет о создаваемой вами авторской программе, избавиться от ошибки поможет переустановка поврежденного приложения. Если не поможет и это — остается обратиться к специалисту, способному исправить кодировку. При этом знайте, что проблема — не в “железе”, а в сбое используемого вами программного обеспечения.