[HN Gopher] The gotcha of unhandled promise rejections
       ___________________________________________________________________
        
       The gotcha of unhandled promise rejections
        
       Author : AshleysBrain
       Score  : 65 points
       Date   : 2023-01-12 00:12 UTC (1 days ago)
        
 (HTM) web link (jakearchibald.com)
 (TXT) w3m dump (jakearchibald.com)
        
       | mirekrusin wrote:
       | So you catch it and pass undefined to your chapter handler
       | function? It will surely blow up.
       | 
       | In production code I'm using `.catch(log.rescueError(msg,
       | returnValue))` helper function (or .rescueWarn) - which is very
       | helpful. It'll log error of that severity and return what I'm
       | providing in case of exception.
        
       | spion wrote:
       | This is why we need proper, rich stream abstractions in JS that
       | integrate well with promises. Promises by themselves are not
       | enough.
       | 
       | Note that a real world implementation of this would also limit
       | the number of concurrent requests - another reason to have
       | streams with rich concurrency options.
        
         | spankalee wrote:
         | Modern JavaScript has fine stream abstractions in WHATWG Stream
         | and async iterables, and Dart has the same unhandled Future
         | issue as JS.
         | 
         | Dart generally has fewer, but nicer, libraries for dealing with
         | collections of Futures, but JS has a lot of npm libraries and
         | is somewhat catching up with additions like
         | Promise.{any,allSettled,race} and Array.fromAsync().
        
           | spion wrote:
           | Async iterables are bare-bones, and WHATWG Streams are not
           | much better.
           | 
           | I removed Dart from the comparison.
        
       | spankalee wrote:
       | I think the ideal thing to do in a lot of cases is to adhere to
       | the spirit of the warning and actually try to handle the
       | rejections in some way.
       | 
       | In this case I'd want to log the error, or display a notice that
       | the chapter couldn't be loaded. For this we need to catch the
       | rejections and turn them into values we can iterate over with for
       | await/of.
       | 
       | Promise.allSettled() does this for us, but forces us to wait
       | until all promises are... settled. A streaming version would give
       | back an async iterable to we could handle promises in order as
       | soon as they're ready. Or we can just convert into the same
       | return type as allSettled() ourselves:                   async
       | function showChapters(chapterURLs) {           const
       | chapterPromises = chapterURLs.map(async (url) => {
       | try {               const response = await fetch(url);
       | return {status: "fulfilled", value: await response.json()};
       | } catch (e) {               return {status: "rejected", reason:
       | e};             }           });                    for await
       | (const chapterData of chapterPromises) {             // Make sure
       | this function renders or logs something for rejections
       | appendChapter(chapterData);           }         }
        
         | [deleted]
        
         | spion wrote:
         | Nice. `Promise.iterateSettled` would be cool.
        
         | horsawlarway wrote:
         | I'm laughing - I just posted basically the exact same thing.
         | This is the right way to solve this.
         | 
         | He's trying to shove error handling into the wrong place (The
         | loop should not be responsible for handling failed requests it
         | knows nothing about).
        
         | crdrost wrote:
         | This code does not recapitulate the desired behavior.
         | 
         | Desired: when the load from chapter 10 fails, even if chapter 2
         | is taking 30s to load, I want to immediately transition the UI
         | into the failure state.
         | 
         | Your code: waits until chapters 1-9 have loaded to report the
         | failure state on chapter 10.
         | 
         | (I posted I believe a correct answer "without the hack" to the
         | OP.)
        
           | spankalee wrote:
           | I'm not trying to say there's only one way to do this,
           | especially if we are talking about user affordances for
           | failure states. I think there could be a debate about showing
           | what data is available vs not showing partial data.
           | 
           | My point is just that trying to do something with the
           | failures often removes the warning and leads to better UX, or
           | at least conscious decisions about it, and usually some clue
           | in the code about what you intend to happen.
        
         | esprehn wrote:
         | I also posted the same thing (and have had this discussion with
         | Jake on Twitter :))
        
       | [deleted]
        
       | [deleted]
        
       | horsawlarway wrote:
       | Personally - he's trying to shove the logic to catch the error
       | into the wrong place.
       | 
       | The loop isn't the right spot to be catching an error that
       | resulted from failing to fetch the data, or to parse the json
       | data that was returned. The right spot was right there at the top
       | of the file...                   const chapterPromises =
       | chapterURLs.map(async (url) => {           const response = await
       | fetch(url);           return response.json();         });
       | 
       | This is where the error is occuring, and this is also the only
       | reasonable spot to handle it. A very simple change to
       | const chapterPromises = chapterURLs.map(async (url) => {
       | try {             const response = await fetch(url);
       | return response.json();           } catch(err) {             //
       | Optionally - Log this somewhere...             return `Failed to
       | fetch chapter data from ${url}` // Or whatever object
       | appendChapter() expects with this as the content.           }
       | });
       | 
       | Solves the entire issue.
        
       | latchkey wrote:
       | This example is interesting because it is doing a fetch in a
       | loop, which could fail for a whole number of reasons and leave
       | you with limited data that you then need to build a UX to deal
       | with ("Some data didn't load correctly, click here to try
       | again"). The example is a fetch of each chapter in a book and
       | this opens up a larger question of how to build APIs.
       | 
       | If I was doing this, I'd have an API endpoint which returns all
       | of the chapters data that I needed for the UX display in a single
       | request. Then, I don't need the loop. The response is all or
       | nothing and easier to surface in the UX.
       | 
       | The next request would presumably just for an individual chapter
       | for all the details of that chapter. Again, no loop.
       | 
       | I know this doesn't answer the question of how to deal with the
       | promises themselves, but more about how to design the system from
       | the bottom up not not need unnecessarily complicated code.
        
         | horsawlarway wrote:
         | Eh, he's not really doing the fetch in a loop - he's generating
         | a sequence of promises that are kicking off the fetch right
         | away (during the call to map).
         | 
         | If the number of chapters is less than the sliding window of
         | outstanding requests in the user's browser - the browser will
         | make all the requests basically in parallel. If the number of
         | chapters is very large, they will be batched by the browser
         | automatically - chromium used to be about 6 connections per
         | host, max of 10 total, not sure if those are the current
         | limits.
         | 
         | He's just processing them in a loop as they resolve. The real
         | issue with his code is that the loop isn't the right place to
         | handle a rejection. The right place is right there at the top
         | of the file in the map call.
         | 
         | The loop doesn't need to care about a failed fetch or json
         | parse, those should be handled in the map, where you can do
         | something meaningful about it. The loop doesn't even have the
         | chapterUrl of the failed call.
        
           | latchkey wrote:
           | Each user is making 6-10 requests, in parallel, to the
           | server, instead of 1. My point is that I'd design this to not
           | DDoS the servers if I got a lot of concurrent users.
        
             | horsawlarway wrote:
             | that's... not a particularly astute approach.
             | 
             | Generally speaking, you're almost always better off with
             | more light requests than fewer heavy requests.
             | 
             | Assuming this is static content (chapters) there's
             | absolutely zilch you're gaining by fetching the whole block
             | at once instead of chapter by chapter, since you can shove
             | it all behind a CDN anyways.
             | 
             | You're losing a lot of flexibility and responsiveness on
             | slow connections, though.
             | 
             | Not to mention - it may make sense to be doing something
             | like fetching say... 5 chapters at a time, and then
             | fetching the next chapter when the user scrolls to the end
             | of the current chapter (pagination - a sane approach used
             | by anyone who's had to deal with data at scale)
             | 
             | In basically every case though, as long as each chapter is
             | at least a few hundred characters, the extra overhead from
             | the requests is basically negligible, and if they're static
             | - easily cached.
        
             | spankalee wrote:
             | Most likely Jake encountered real world cases that really
             | needed to use parallel promises and used fetch() as an
             | example that could fit in a blog.
             | 
             | Maybe in the real world it's file or database reads, off-
             | thread parsing, decompression, etc...
        
               | latchkey wrote:
               | Yes, this is why I wrote:
               | 
               | > _I know this doesn 't answer the question of how to
               | deal with the promises themselves_
               | 
               | I'm trying to bring up a bigger picture here.
        
       | pyrolistical wrote:
       | This seems like a bug with the unhandled rejected promise
       | handler.
       | 
       | IMO it should only trigger for unhandled promises that are
       | garbage collected. This way the given example wouldn't cause a
       | false positive
        
       | neallindsay wrote:
       | I was strongly against JS engines treating unhandled rejections
       | as errors because a rejected promise is only ever not handled
       | _yet_. Basically, in order to prevent blow-ups from unhandled
       | rejections, you have to synchronously decide how to handle
       | rejections. I feel like this goes against the asynchronous nature
       | of promises.
        
       | pohl wrote:
       | This needs a trigger warning. I flashed back to upgrading a large
       | project's build from node 12 to 16, which introduced a bunch of
       | these in the scope of jest tests, and there was no indication of
       | where the offending code was, which made the correct advice of
       | "handle the rejections in some way" a little difficult.
        
       | samsquire wrote:
       | It's similar to manual memory management: you have to remember to
       | do the other side of the thing you're doing.
       | 
       | Structured concurrency is one approach to solving this problem.
       | In a structured concurrency a promise would not go out of scope
       | unhandled. Not sure how you would add APIs for it though in
       | Javascript.
       | 
       | See Python's trio nurseries idea which uses a python context
       | manager.
       | 
       | https://github.com/python-trio/trio
       | 
       | I'm working on a syntax for state machines and it could be used
       | as a DSL for promises. It looks similar to a bash pipeline but it
       | matches predicates similar to prolog.
       | 
       | In theory you could wire up a tree (graph if you have joins) of
       | structured concurrency with this DSL.
       | 
       | https://github.com/samsquire/ideas4#558-assign-location-mult...
        
         | tech2 wrote:
         | Python 3.11 introduced asyncio.TaskGroup[0] to cover the same
         | use-case as trio nurseries (scoped await on context manager
         | exit). It's imperfect, sure, but it improves matters.
         | 
         | [1] https://docs.python.org/3/library/asyncio-
         | task.html#asyncio....
        
       | macrael wrote:
       | yet another reason I try and avoid throwing/rejecting promises in
       | typescript code and just return `Thing | Error` everywhere. I'm
       | sure there's something fancier I could get out of using a full
       | Result type but this gets me a compiler enforcing I handle all
       | errors somehow and keeps `try catch` out of my business logic.
        
         | candiddevmike wrote:
         | Huh, never thought to do this. Coming from Go, this would
         | honestly be very ergonomic (not being snarky).
        
           | hot_gril wrote:
           | Well you know what they say, the better name for Go would've
           | been Errlang.
        
           | kansface wrote:
           | JS let's you throw any type as an error.
        
         | tluyben2 wrote:
         | This is what we do; for me it came from ancient times when I
         | was doing fulltime C dev and I mostly kept it up (but improved)
         | except when working on teams that want or already have
         | exceptions all over in all layers.
        
         | hot_gril wrote:
         | That's ok, and I get the purity of only using the response
         | instead of an implicit exception, but it gets tedious when
         | probably almost every func can return an error. You only have
         | to handle exceptions at the outermost layer. I don't understand
         | why this is seen as a "gotcha" in the article, it's the whole
         | point of exceptions.
        
         | rickstanley wrote:
         | I do it the "go" way, with:                 type Result<K =
         | any, T extends Error = Error> = [K, null] | [null, T];
         | 
         | And use it:                 const [response, error] = await
         | tryFetch<Type>("");       if (error) { handle(); }
        
           | macrael wrote:
           | But this way you are losing the type checker yelling at you
           | if you try and use response without having handled error.
           | What I like about                   res = something(): Thing
           | | Error
           | 
           | is I have to do                   if (res instanceof Error) {
           | handle()           return res         }
           | doSomethingWith(res)
           | 
           | or else the compiler will yell at me for trying to
           | doSomethingWith(Error)
        
       | esprehn wrote:
       | I disagree that the fix is to catch and ignore the error. The
       | fetch handling logic should be catching the errors and reporting
       | them somewhere (ex. Sentry or the UI). In a production app where
       | you report and log errors this "issue" doesn't manifest.
        
         | akira2501 wrote:
         | I usually catch rejections then turn them into a resolution as
         | an object with an 'errno' property. That way, your handler
         | always gets called, rejections are never generated up to the
         | top level, and you can move the error handling into your
         | consumer very easyily.                   const thing = await
         | fn();         if (thing?.errno) {             //handle error
         | return;         }              // handle result
         | 
         | Maybe I spent too much time writing C code. There are very few
         | times when I actually want a rejection to take an entirely
         | different path through my code, and this feels like the correct
         | way to write it.
        
       ___________________________________________________________________
       (page generated 2023-01-13 23:00 UTC)