diff --git a/content/developer/reference/frontend.rst b/content/developer/reference/frontend.rst index cc65ce4a6..e2b7fae34 100644 --- a/content/developer/reference/frontend.rst +++ b/content/developer/reference/frontend.rst @@ -16,6 +16,7 @@ Web framework frontend/services frontend/hooks frontend/patching_code + frontend/error_handling frontend/javascript_reference frontend/mobile frontend/qweb diff --git a/content/developer/reference/frontend/error_handling.rst b/content/developer/reference/frontend/error_handling.rst new file mode 100644 index 000000000..82b7a6930 --- /dev/null +++ b/content/developer/reference/frontend/error_handling.rst @@ -0,0 +1,588 @@ +============== +Error handling +============== + +In programming, error handling is a complex topic with many pitfalls, and it can +be even more daunting when you're writing code within the constraints of a framework, +as the way you handle errors needs to mesh with the way the framework dispatches +errors and vice versa. + +This article paints the broad strokes of how errors are handled by the JavaScript +framework and Owl, and gives some recommendations on how to interface with these +systems in a way that avoids common problems. + +Errors in JavaScript +==================== + +Before we dive into how errors are handled in Odoo as well as how and where to +customize error handling behavior, it's a good idea to make sure we're on the +same page when it comes to what we mean exactly by "error", as well as some of +the peculiarities of error handling in JavaScript. + +The `Error` class +----------------- + +The first thing that may come to mind when we talk about error handling is the +built-in `Error` class, or classes that extend it. In the rest of this article, +when we refer to an object that is an instance of this class, we will +use the term *Error object* in italics. + +Anything can be thrown +---------------------- + +In JavaScript, you can throw any value. It is customary to throw *Error objects*, +but it is possible to throw any other object, and even primitives. While we don't +recommend that you ever throw anything that is not an *Error object*, the Odoo +JavaScript framework needs to be able to deal with these scenarios, which will +help you understand some design decisions that we've had to make. + +When instanciating an *Error object*, the browser collects information about +the current state of the "call stack" (either a proper call stack, or a reconstructed +call stack for async functions and promise continuations). This information is +called a "stack trace" and is very useful for debugging. The Odoo framework displays +this stack trace in error dialogs when available. + +When throwing a value that is not an *Error object*, the browser still collects +information about the current call stack, but this information is not available +in JavaScript: it is only available in the devtools console if the error is not +handled. + +Throwing *Error objects* enables us to show more detailed information, which a +user will be able to copy/paste if needed for a bug report, but it also makes +error handling more robust as it allows us to filter errors based on their class +when handling them. Unfortunately, JavaScript does not have syntactic support for +filtering by error class in the catch clause, but you can relatively easily do +it yourself: + +.. code-block:: javascript + + try { + doStuff(); + } catch (e) { + if (!(e instanceof MyErrorClass)) { + throw e; // caught an error we can't handle, rethrow + } + // handle MyErrorClass + } + +Promise rejections are errors +----------------------------- + +During the early days of Promise adoption, Promises were often treated as a way +to store a disjoint union of a result and an "error", and it was pretty common to +use a Promise rejection as a way to signal a soft failure. While it might seem like +a good idea at first glance, browsers and JavaScript runtimes have long +started to treat rejected Promises the same way as thrown errors in pretty much +every way: + +- throwing in an async function has the same effect as returning a Promise that is + rejected with the thrown value as its rejection reason. +- catch blocks in async functions catch rejected Promises that were awaited in the + corresponding try block. +- runtimes collect stack information about rejected promises. +- a rejected Promise that is not caught synchronously dispatches an event on + the global/window object, and if `preventDefault` is not called on the event, + browsers log an error, and standalone runtimes like node kill the process. +- the debugger feature "pause on exceptions" pauses when Promises are rejected + +For these reasons, the Odoo framework treats rejected Promises in the exact same +way as thrown errors. Do not create rejected promises in places where you would +not throw an error, and always reject Promises with *Error objects* as their rejection +reason. + +`error` events are not errors +----------------------------- + +With the exception of `error` events on the window, `error` events on other objects +such as ``, `