graphql

2021-02-04T14:59:24.013700Z

Hey. If a lacinia resolver throws an exception, this exception is passed back in a resolve-promise as a value (here: [1]). Now, if a resolver returns a resolve-promise itself, and crashes in trying to do something asynchronously, we can’t seem to pass the exception we got to lacinia-pedestal: we can’t throw it because the resolver already completed, its lacinia waiting for us to deliver on our result-promise We can’t deliver the exception as value to our result-promise as it then appears to be read as a ‘normal’ successful value (here: [2]). What is the conventional wisdom here? [1]: https://github.com/walmartlabs/lacinia/blob/83ee169e4aa436bba56b8b725a03aae0e6bffb26/src/com/walmartlabs/lacinia/executor.clj#L465-L466 [2]: https://github.com/walmartlabs/lacinia/blob/83ee169e4aa436bba56b8b725a03aae0e6bffb26/src/com/walmartlabs/lacinia/executor.clj#L450-L464

1➕
hlship 2021-02-04T16:52:33.015900Z

You can use a try block, and, convert the exception to an error map, then wrap your result (even nil) via resolve/with-error. The 3-arity deliver! method is just a convenience around with-error (well, it predates with-error, but still).

2021-02-04T17:02:39.016Z

Would there be a way to get the ex through the pedestal stack? Handeling exceptions is already setup there (monitoring, reporting, default internal-server-error-response etc).

hlship 2021-02-04T17:21:32.016300Z

There really isn't, resolver functions aren't supposed to fail, they're supposed to deliver an error map. We catch exceptions when calling the field resolver initially (mostly as a convenience during development).

hlship 2021-02-04T17:22:12.016500Z

One approach that might work is to add an interceptor to the chain that puts an Atom into the context; on leave the value in the Atom is deref'ed and, if non-nil, re-thrown.

hlship 2021-02-04T17:24:39.016700Z

But this is an interesting use case and I have to wonder if there's a way to restructure things to make it easier to propogate a thrown exception out from even an async callback but maintain the useful things that Lacinia does (identify field, arguments, etc.) when an exception is thrown by synchronous field resolver.

1👀
2021-02-04T17:36:03.017Z

The atom suggestion is pretty good. Could even watch the atom and halt the resolving upon an uncaught exception. All 'expected' exceptions are handled in the resolver and are returned using with-errors or a similar construct. For any exception we didn't expect, we use an interceptor as a last-line-of-defence. It which logs the error, pushes it to rollbar (/sentry) and returns a generic error message to the frontend. In a perfect world that interceptor wouldn't be hit, but you know... After seeing the link @lennart.buit shared, I assumed lacinia to rethrow if a throwable was delivered to a ResolverResult. But I guess that's on me 😅

hlship 2021-02-04T18:09:52.017200Z

Nailing down the right approach to exceptions is always a challenge, and that goes double for any kind of asynchronous processing. We have similar issues in use of core.async. I like how in Elixir, it's always about a receive and that always has a timeout.

1👀
hlship 2021-02-04T18:10:37.017400Z

Sorry, segued there from exceptions to dealing with async in general (there's an overlap, where exceptions may impact delivery of async results).

2021-02-04T20:23:27.023400Z

Yeah agree, in lacinia-pedestal (or pedestal for that matter) we (or I) had the same issue in dealing with an exception where there were also differences in how to handle sync/async exceptions. Iirc, you can ‘just’ throw in a sync interceptor, but you need to return an ex in a specific way in the context in an async one. Certainly not an easy issue

2021-02-04T20:49:41.027500Z

Maybe that’s something to take inspiration from, pedestals context contains an error key for asynchronous delivery of exceptions. Definitely didn’t think this through, but sounds like something that could work (e.g. resolve/with-exception)

hlship 2021-02-04T23:24:31.027700Z

That's an idea.