Skip to content

The server crashes if a promise with deferred handling rejects while another promise is trying to resolve #7857

@mfanegg

Description

@mfanegg

Issue Description

If an unhandled error and/or promise rejection occurs in a resolver, the expectation is that the error will be caught by the Apollo Server and added to an errors array in the response.

For example, this resolver:

...
const myResolver = async () => {
  return Promise.reject(new Error('I am rejecting.'));
}

const resolvers = {
  Query: {
    hello: myResolver
  },
};
...

should return something like this when queried:

{
  "data": {
    "hello": null
  },
  "errors": [
    {
      "message": "I am rejecting.",
...

However, "asynchronously handled" rejections (I'm not sure if my terminology is right) crash the server if the rejection occurs while another await is happening. Example:

// the crash only happens if REJECTION_TIMER_MILLIS < RESOLUTION_TIMER_MILLIS
const REJECTION_TIMER_MILLIS = 1000;
const RESOLUTION_TIMER_MILLIS = 3000;

const thisPromiseWillReject = () => {
  return new Promise((_, reject) => setTimeout(() => {
    reject(new Error('I am rejecting.'));
  }, REJECTION_TIMER_MILLIS));
}

const thisPromiseWillResolveAfterALongTime = () => {
  return new Promise((resolve) => setTimeout(resolve, RESOLUTION_TIMER_MILLIS));
}

const myResolver = async () => {
  const x = thisPromiseWillReject();
  await thisPromiseWillResolveAfterALongTime();
  await x;
}

const resolvers = {
  Query: {
    hello: myResolver
  },
};

It looks like the promise has to be rejected while another await is still pausing execution. If the rejection happens faster than the await right after it, then the error is handled as expected.

For further info, here is what my terminal looks like (my code is in a file called "server.mjs", and I'm using the simple standalone example from the apollo-server README):

mfan@mymachine mock-apollo-server-for-issue % node server.mjs
🚀 Server ready at http://localhost:4000/
file:///Users/mfan/dev/mock-apollo-server-for-issue/server.mjs:10
    reject(new Error('I am rejecting.'));
           ^

Error: I am rejecting.
    at Timeout._onTimeout (file:///Users/mfan/dev/mock-apollo-server-for-issue/server.mjs:10:12)
    at listOnTimeout (node:internal/timers:559:17)
    at processTimers (node:internal/timers:502:7)
mfan@mymachine mock-apollo-server-for-issue % 

I also have in this submission a link to a repo with the reproduction.

I was also able to repro this with .then syntax instead of async/await. This resolver will also crash the server:

const myResolverC = async () => {
  const x = thisPromiseWillReject();
  thisPromiseWillResolveAfterALongTime().then(() => {
    x.catch((err) => {
      console.log('caught error in resolver C')
    })
  });
}

I know that my repro code is not ideal -- promise rejections should be handled -- but I wouldn't expect this to take down the whole server. Could this be investigated?

Link to Reproduction

https://github.com/mfanegg/mock-apollo-server-for-issue-2

Reproduction Steps

  1. clone the reproduction repo
  2. make sure you're using node v16 (I haven't tried versions over 16 but maybe those will work)
  3. in a terminal, run npm i
  4. run node server.mjs
  5. run the operation query { hello }
  6. go to the terminal and observe that the server has exited

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions