segmentio / analytics-node

The hassle-free way to integrate analytics into any node application.

Home Page:https://segment.com/libraries/node

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unhandled Promise Rejection Can Crash App

GCAndrew opened this issue · comments

As described in #320 , ‘flush()’ can cause an uncaught exception. Listening for uncaught exception events isn’t an ideal error handling pattern.

This exception should be able to be handled at the call-site of ‘track()’ and similar functions.

Example PR: #327

@GCandrew your PR adds a property that could be used to "turn off" error throwing once the axios request (promise) is rejected, I got nothing against this idea.

Reading the code, I am getting the impression that the idea here is to pass the possible error to any callback sent to flush and the individual message's callbacks, then throwing the exception again allows the promise to continue its normal course, which seems to be the problem here; my only question is: wouldn't it be better just to add an optional parameter to flush instead of making this a concern of the analytics client.

  flush (callback, noUnhandledRejection = false) {
    callback = callback || noop
    ....
    .catch(err => {
        if (err.response) {
          const error = new Error(err.response.statusText)
          done(error)
          if (!noUnhandledRejection) {
            throw error
          }
        }

        done(err)
        if (!noUnhandledRejection) {
          throw err
        }
    })

Hi, @emont01. My concern lies with enqueue(...) which calls flush when the queue hits its size limit. This is a useful behavior, but it means that the consumer of this package cannot always pass arguments to flush() unless they always manually flush and override queue limits.

The issue I encounter is:
Consumer app calls track() -> calls enqueue() -> calls flush() @

this.flush()
-> promise rejection
And the promise rejection goes unhandled at the flush() call-site in enqueue().

@GCandrew sounds good to me! I would suggest your PR makes the behavior the default

this.noUnhandledRejection = options.noUnhandledRejection || true

@emont01 Edit: Thinking about it, and reading the tests, maybe it should stay defaulted to false. Have it be an opt-in behavior to preserve compatibility for users relying on the rejection when manually calling flush(). Thoughts?

@GCandrew yes! The default value of noUnhandledRejection should be false IMHO, my main concerns right now is how setting it to true will break the retry behavior, we should let axios-retry do its thing before handling any rejection.

To be honest I completely forgot about 'axios-retry' being used and after going over how that works [1] now I think some changes maybe necessary in either the tests, your PR or both.

axios-retry documentation does not mention any way to capture the "final error", at that moment we could handle the exception and avoid an unhandled rejection, possibly something like [2].

I will try with my suggested try-catch changes and let you know if they work, let me know if you see a better way.

[1]

  1. axios-retry catches any error and then checks if it is possible to retry
  2. axios-retry will then return a promise with a delay or reject

[2]

  flush (callback) {
    ....
    try {
      return await this.axiosInstance.post(`${this.host}${this.path}`, data, req)
        .then(() => {
          done()
          return Promise.resolve(data)
        })
        .catch(err => {
          if (err.response) {
            const error = new Error(err.response.statusText)
            done(error)
            throw error
          }

          done(err)
          throw err
        })
    } catch (err) {
        if (!this.noUnhandledRejection) {
          throw err
        }
    }
  }

hey @GCandrew I have checked a few ideas but here are the main issues I see with the exception handling change:

  1. some tests rely on flush throwing an exception [1], so resolving the promise instead makes those test fail
  2. axios-retry "catches" errors to determine if a retry should take place or not

We could use a try-catch and wait for axios-retry to give up and handle any exception based on noUnhandledRejection's value but then we would be back to square 1 with some tests failing depending on said variable's value.

[1] image

@GCandrew Closing this issue, thank you.

@GCandrew Closing this issue, thank you.

Hi, excuse me, what is the recommended way to catch promise rejections as a user of this library?

@nd4p90x Forgot to mention you, sorry!