Description
I'm running into a pretty bizarre error while trying to upgrade our app to ember-concurrency@2.0.2
(from the latest pre-2.0.0
release).
It seems that the async error handler defined here is in fact running synchronously, and prempting our own error handling.
We have a swallow-error
helper that we sometimes use for cases where an Ember Concurrency task would expect an error to be thrown and we use the derived error state on the task instance to show it to the user. Because we error will be "handled" in the UI itself, we want to swallow the error entirely.
swallow-error
helper source
import { helper } from '@ember/component/helper';
/**
* This helper is meant to be wrapped around another action that might throw an error that we want to suppress.
*
* ```hbs
* <button {{on 'click' (swallow-error (perform this.someTaskThatMightThrow))}}>
* Click Me
* </button>
* ```
*/
export function swallowError([fn]) {
return function callAndSwallowError(...args) {
try {
const response = fn(...args);
if (response.catch) {
return response.catch(function () {
// Swallow async error
});
}
return response;
} catch (e) {
// Swallow synchronous error
}
};
}
export default helper(swallowError);
In the current version of ember-concurrency
that we're using, this works just fine.
In ember-concurrency@2.0.2
, it seems like the code here that should report the error if it was not already caught is being executed synchronously when it should be executed asynchronously
ember-concurrency/addon/-private/external/task-instance/executor.js
Lines 344 to 348 in 35a188c
I know it's a challenge to grok a stack trace without much context, but this is what I'm seeing; a single synchronous trace from my debugger
point inside the error-reporting callback that should be run asynchronously straight down to the function invocation inside callAndSwallowError
.
I know that env.async
is using join
from the @ember/runloop
package, which will insert the callback into the current queue. My guess is that Backburner is calling things in the current queue before proceeding with execution of the code in my helper... for some reason. My code isn't doing anything directly with the runloop; there's just the normal amount of wrapping that helper invocations have by default at play.
I tried to create a failing test case in the ember-concurrency
repository but have been unable to get the same error to occur there just yet. I will post a PR with that once I have it.
I'm wondering whether a potential fix could be specifically scheduling the async
callback into the next run loop, rather than using join
which can insert it into the current "tick".
Metadata
Metadata
Assignees
Projects
Milestone
Relationships
Development
- Remove runloop binding for perform helpermachty/ember-concurrency
- Bump ember-concurrency from 2.0.2 to 2.0.3alexlafroscia/ember-concurrency-wrap-in-task
- chore(deps-dev): bump ember-concurrency from 2.0.2 to 2.0.3alexlafroscia/ember-steps
- Bump ember-concurrency from 2.0.2 to 2.0.3brunoocasali/chuck-norris-facts
Activity
alexlafroscia commentedon Mar 3, 2021
Comparing stack traces between my app (with the problem) and the
ember-concurrency
tests (without the problem) is that in my tests, at the point that the task is performed (while wrapped in my promise-swallowing helper) the run-loop is empty, and thus a new one is created, while in theember-concurrency
test the runloop is already populated.In terms of the actual methods being queued, in this code:
https://github.com/BackburnerJS/backburner.js/blob/6a720fcf1286b500e7bbf892bc25a055c779696c/lib/index.ts#L622-L624
The Ember Concurrency async error-handler is run synchronously if
currentInstance
isnull
; if not, the async error handler does what you would expect!maxfierke commentedon Mar 3, 2021
Mind sharing some code from the task too? Mostly curious about whether the error is happening before or after the first
yield
.alexlafroscia commentedon Mar 3, 2021
Before -- this is the task definition
alexlafroscia commentedon Mar 3, 2021
Interestingly, I updated
ember-qunit
to the latest version in theember-concurrency
repo, and sure enough, the test I wrote in theember-concurrency
test suite is now also failing! So in some way,ember-qunit
influences the state of Backburner at the point that the code is running, which influences whether the "async error handler" is actually run asynchronously or not!alexlafroscia commentedon Mar 3, 2021
Maybe, to ensure that the async error handler is actually called asynchronously, we could use
later
from@ember/runloop
instead, with a timeout of0
or1
? I'm not sure if0
queues for the next "tick" in the same way assetTimeout
, but I would assume it does.maxfierke commentedon Mar 3, 2021
@alexlafroscia can you share the test case here or in a PR?
Using
later
with1
ornext
might work, but I'll need to do some thinking about what makes sense here.later
with1
is already used byreportUncaughtRejection
, so I wonder if that might effectively make it two "ticks"?I think we roughly used
run
in 1.x (withsetTimeout
if there was no runloop... :/) so maybe there's some subtlety of nested runloops that would help here, but I'd have to play around with it.alexlafroscia commentedon Mar 4, 2021
@maxfierke very-much-messy PR opened with the test I wrote into the test suite that fails
maxfierke commentedon Mar 4, 2021
having a bit of difficulty reducing this, but curious what happens if you do
const response = await fn(...args);
instead (then you probably don't need the.catch
check and call, as it'll all be handled by the normaltry/catch
semantics)Tried reproducing it outside of a helper, and I can't :/
maxfierke commentedon Mar 12, 2021
Alrighty, I think I'm starting to get a handle on this. To be clear: the error reporting is happening asynchronously, but there's an ordering issue in which the uncaught error reporting is running before the
catch
chain has been called on theTaskInstance
, so the internal flag for async error handling hasn't been set tofalse
by the time the error reporting code runs. I'm not 100% sure why the delay. Maybe has to do with how helpers are evaluated or something (or theperform
helper in particular)Seemed to be able to fix the failing tests by wrapping the call in
Promise.resolve().then(...)
to push it to the next tick while within the runloop, so that thecatch
has a chance to register itself before the throw happens. Test suite passes, but need to think more about whether this is more correctmaxfierke commentedon Mar 15, 2021
Alright, root cause here seems to be due to the
run.bind
wrapping of theperform
helper. Basically, it's wrappingperform
in a full runloop, which means that a synchronous task throw will be reported within that same invocation, which is before the(swallow-errors)
helper is able to come in an instrument the task instance. Since it's already run, it's too late. I don't think we need this runloop binding anymore, since there's no auto-run assertion with the versions supported by ember-concurrency 2.x, so I will look into whether it's a breaking change to simply remove it. FWIW, the test suite likes it fine, but I want to run it against a bigger, more complex app in order to ensure it's not creating additional problems. Will post a PR once I verify it doesn't cause any issues.alexlafroscia commentedon Mar 16, 2021
Sounds good! Thanks for getting to the bottom of this!!
alexlafroscia commentedon Mar 17, 2021
Thanks again @maxfierke for putting so much effort into this admittedly-obscure bug!