Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly handle rejection errors in async event listeners in Vuejs?

What is the proper way to call an async function from an event listener in vue.js, while ensuring that any promise rejection errors will bubble up via onErrorCaptured like normal?

My first instinct would be to just add async:

window.addEventListener('click', async () => {
  await toggle() // error: no-misused-promise
})

However Vue onErrorCaptured won't be called if the promise is rejected and this violates the eslint rule @typescript-eslint/no-misused-promise

Another option would be to remove both await and async:

window.addEventListener('click', () => {
  toggle() // error: no-floating-promises
})

However this still leads to unhandled promise rejections and violates the rule @typescript-eslint/no-floating-promises.

I looked for some way to manually send the rejection error to be handled by Vue onErrorCaptured, Vue exports a function "handleError" but it seems to be for internal use only.

window.addEventListener('click', () => {
  toggle().catch(handleError) // argument error
})

My current workaround is to store the error in a ref and throw from a watchEffect handler:

const error = ref<Error>()

window.addEventListener('click', () => {
  toggle().catch((err => { error.value = err })
})

watchEffect(() => {
  if (error.value) {
    throw new Error(error.value)
  }
})

How have other people solved this?

like image 204
doeke Avatar asked Sep 16 '25 21:09

doeke


1 Answers

The errors that don't come from Vue API (lifecycle hooks, watchers, etc) and template event listeners aren't handled by Vue, this needs to be done by a user.

If asynchronous error needs to be ignored, this needs to be explicitly done because it would result in unhandled promise error otherwise:

window.addEventListener('click', async () => {
  try {
    await toggle()
  } catch {}
})

Or:

window.addEventListener('click', () => {
  toggle().catch(() => {});
})

Error handling can be done roughly the way it was done here. Some things that can be improved, new Error(error.value) would discard the original call stack and result in wrongly formatted error message for error.value being an instance of Error, which is expected. Generally it's preferable to rethrow an error in this case.

In order to handle synchronous errors, synchronous watcher would be suitable more.

It could be extracted to such composable:

const useHandleComponentError = () => {
  const error = ref();

  watchSyncEffect(() => {
    if (error.value === undefined) {
      return;
    } else if (error.value instanceof Error) {
      throw error.value;
    } else {
      throw new Error(String(error.value))
    }
  });
  
  return fn => function errorHandler(...args) {
    let result;
    try {
      result = fn.apply(this, args);
    } catch (err) {
      // Sync error
      error.value = err;
    }
    
    if (result?.then) {
      // Promise-like async error
      result.then(null, err => {
        error.value = err;        
      });
    }
  };
};

And used like:

const handleComponentError = useHandleComponentError();

window.addEventListener('click', handleComponentError(async () => {
  throw new Error('whatever')
}))
like image 111
Estus Flask Avatar answered Sep 19 '25 14:09

Estus Flask