Improving axios errors with useful stack traces

Axios has some issues with errors, and it is even worse on SSR since the only thing you get when accessing the page is something like this:

Error: Request failed with status code 500
    at createError (/home/rafael/project/node_modules/axios/lib/core/createError.js:16:15)
    at settle (/home/rafael/project/node_modules/axios/lib/core/settle.js:17:12)
    at IncomingMessage.handleStreamEnd (/home/rafael/project/node_modules/axios/lib/adapters/http.js:236:11)
    at IncomingMessage.emit (events.js:203:15)
    at endReadableNT (_stream_readable.js:1129:12)
    at process._tickCallback (internal/process/next_tick.js:63:19)

Which is not useful at all, look at this stack trace! We have no clue where the request was done, how can we fix this? There are issues like axios/axios#2387 and axios/axios#2069 but they don't have any solutions, so we need to solve this ourselves.

First of all, it is highly recommended to have a module to abstract axios and provide functions for each operation, so I will consider you have a get function that is similar to this:

export const get = (endpoint, params, config) => {
  // instance here is the return of axios.create
  return instance.get(endpoint, { params, ...config });
};

My first attempt was this:

export const get = (endpoint, params, config) => {
  return instance.get(endpoint, { params, ...config }).catch((error) => {
    throw new Error('hello!');
  });
};

But turns out even my error can't maintain the stack trace, this is happening because the function is called asynchronously, so the old stack trace is already lost. The trick here is to maintain the old stack trace before throwing the error, and I learned something:

The stack trace isn't something special, it is just a string.

Since it is just a string, we can store it before doing the request, and assign it to the error before throwing it:

export const get = (endpoint, params, config) => {
  const { stack } = new Error();
 
  return instance.get(endpoint, { params, ...config }).catch((error) => {
    error.stack = stackTrace;
    throw error;
  });
};

And now our error contains the old stack trace, so we know exactly where the code has been called! To improve the current code we can move our catch function to somewhere else and output the response body in the error message, this is the full code:

export const axiosCatch = (stackTrace) => (error) => {
  let errorMessage = error.message;
 
  if (error.response) {
    const responseBody = JSON.stringify(error.response.data, null, 2);
    errorMessage = `Request failed with status code ${error.response.status}\n`;
    errorMessage += `Response body: ${responseBody}`;
  }
 
  error.message = errorMessage;
  error.stack = stackTrace;
 
  throw error;
};

And to use it:

export const get = (endpoint, params, config) => {
  const stackTrace = getStackTrace();
 
  return instance
    .get(endpoint, { params, ...config })
    .catch(axiosCatch(stackTrace));
};

I also moved the stack trace code into another function to repeat it in the other methods and I am manipulating it to remove the getStackTrace function from the stack trace, you can tweak it as you like:

const getStackTrace = () => {
  const { stack } = new Error();
  let split = stack.split('\n');
 
  // Remove the above "new Error" line from the stack trace
  if (split[1].includes('at getStackTrace')) {
    split = [split[0], ...split.splice(2)];
  }
 
  return split.join('\n');
};