Problem
I'm adding an error boundary to my client-side React app. In development, I want to display the error with a stack trace in the browser window, similar to create-react-app's or nextjs's error overlays. Using webpack's devtool
option, I'm able to generate a stack trace with the correct filename, but the wrong line number.
// This is what renders in the browser window
Error: You got an error!
at ProjectPage (webpack-internal:///./src/pages/ProjectPage.tsx:96:11) // <-- 96 is the wrong line
// This is what shows up in the console
Uncaught Error: You got an error!
at ProjectPage (ProjectPage.tsx?8371:128) // <-- 128 is the correct line
What I've tried
- This answer suggests different
devtool
settings, but none of the ones I've tried provide correct line numbers. - This answer suggests changing the
retainLines
babel setting in webpack, but I'm not using babel to transpile my code, I'm using ts-loader. Also, the babel docs suggest this option is a workaround for people not using source maps, which shouldn't be an issue here. - This answer suggests an external library to parse the stack trace. I tried it, but it simply parses the existing trace into objects and the line numbers are still wrong.
- The React docs suggest using
babel-plugin-transform-react-jsx-source
but again, I'm not using babel to transpile my code. Should I be?
I'm not sure if this is a problem with ts-loader, webpack, or some other fundamental step I'm not understanding about source mapping. Setting a debugger in componentDidCatch
and inspecting the error gives me the wrong line number, but when it gets logged to the console it's correct. It seems that the console has an additional step to map the correct line numbers; is this something I need to do manually?
ErrorBoundary.tsx
class ErrorBoundary extends React.Component {
state = {
error: null,
};
static getDerivedStateFromError(error) {
return {
error,
};
}
componentDidCatch(error, errorInfo) {
// Line numbers are wrong when inspecting in the function, but correct when logged to the console.
console.log(error, errorInfo);
}
render() {
return this.state.error ?
<ErrorPage error={this.state.error} /> :
this.props.children;
}
}
ErrorPage.tsx
const ErrorPage = ({ error }) => {
if (__DEV__) {
return (
<Layout title={error.name}>
<h1>{error.name}: {error.message}</h1>
<pre>{error.stack}</pre>
</Layout>
);
}
// Display a nicer page in production.
};
tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true,
"jsx": "react",
"lib": ["es2015", "dom"],
"module": "commonjs",
"sourceMap": true,
"target": "es6"
}
}
webpack.config.js
module.exports = (env, argv) => {
return {
mode: isProduction ? 'production' : 'development',
output: {
path: path.join(__dirname, env.output_path),
filename: 'app.bundle.js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
devtool: isProduction ? 'source-map' : 'eval-source-map',
entry: ['./src/index.tsx'],
module: {
rules: [
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
...
],
},
devServer: {
contentBase: path.join(__dirname, env.output_path),
disableHostCheck: true,
historyApiFallback: true,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
},
},
};
};