Handling unhandled promise rejections in async functions

Gajus Kuizinas
Nov 2 · 3 min read

I am developing a HTTP proxy service and I have observed presence of an odd error message in my logs:

unhandledRejection RequestError: HTTP request error.
at /dev/rayroute/raygun/src/factories/createRequest.js:107:13
at processTicksAndRejections (internal/process/task_queues.js:93:5) {
code: 'RAYGUN_REQUEST_ERROR',
originalError: Error: test
at /dev/rayroute/raygun/src/factories/createRequest.js:73:29
at processTicksAndRejections (internal/process/task_queues.js:93:5)

It is odd because there are plethora of tests to ensure that all errors are handled. It is also odd because I have never never seen unhandled rejection while developing the service (only ever saw it in production logs).

The relevant code looks like this:

const activeRequestHandler = createRequest(requestDefinition);if (incomingMessage.socket) {
incomingMessage.socket.on('close', () => {
if (responseIsReceived) {
log.trace('client disconnected after response');
} else {
log.debug('client disconnected');
activeRequestHandler.abort(new Error('CLIENT_DISCONNECTED'));
}
});
}
try {
await actions.afterRequestActions(
context,
requestDefinition,
activeRequestHandler
);
} catch (error) {
log.error({
error: serializeError(error),
}, 'afterRequest threw an error');
}
try {
responseDefinition = await activeRequestHandler.response;
} catch (error) {
log.warn({
error: serializeError(error),
}, 'an error occurred while waiting for a HTTP response');
// [..]
}

It is pretty straightforward:

  • createRequest initiates a HTTP request and returns a request handler
  • the request handler can be used to abort the ongoing request (afterRequestActions aborts request after a hard-timeout); and
  • it is used to resolve the response or eventual rejection of the promise

I have written tests to ensure that errors are handled when:

  • request rejected
  • request aborted
  • afterRequestActions throws an error

, but all tests are passing.

It turns out that the problem was that in all my test cases actions.afterRequestActions was resolving/ being rejected before activeRequestHandler.response is resolved. Meanwhile, in production afterRequestActions contains logic that can take substantially longer to execute. I have also learned that even if you declare a try..catch block for your async function, if it resolves before it is await-ted, then you will get an unhandled rejection, i.e.

This code will not warn about unhandled rejection:

const delay = require('delay');const main = async () => {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Expected rejection.'));
}, 100);
});
await delay(90); try {
await promise;
} catch (error) {
console.error(error)
}
};
main();

But this code will always produce a warning about an unhandled rejection:

const delay = require('delay');const main = async () => {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Expected rejection.'));
}, 100);
});
await delay(110); try {
await promise;
} catch (error) {
console.error(error)
}
};
main();

The best solution is to add an auxiliary catch block, e.g. This is how I refactored my original code:

const activeRequestHandler = createRequest(requestDefinition);// Without this we were getting occasional unhandledRejection errors.
// @see https://dev.to/gajus/handling-unhandled-promise-rejections-in-async-functions-5b2b
activeRequestHandler.response.catch((error) => {
log.warn({
error: serializeError(error),
}, 'an error occurred while waiting for a HTTP response (early warning)');
});
if (incomingMessage.socket) {
incomingMessage.socket.on('close', () => {
if (responseIsReceived) {
log.trace('client disconnected after response');
} else {
log.debug('client disconnected');
activeRequestHandler.abort(new Error('CLIENT_DISCONNECTED'));
}
});
}
try {
await actions.afterRequestActions(
context,
requestDefinition,
activeRequestHandler
);
} catch (error) {
log.error({
error: serializeError(error),
}, 'afterRequest threw an error');
}
try {
responseDefinition = await activeRequestHandler.response;
} catch (error) {
log.warn({
error: serializeError(error),
}, 'an error occurred while waiting for a HTTP response');
// [..]
}

Written by

Software architect, startup adviser. Editor of https://medium.com/applaudience. Founder of https://go2cinema.com.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade