Enhancing Esbuild Error Handling in a Rails App.

Published on January 30, 2025
Written by Victor Cobos

When working on a Rails app using esbuild, encountering build errors can be frustrating β€” especially when they don't surface in a way that makes debugging easier. To improve the developer experience, I implemented an automatic error rendering system that displays esbuild errors directly in the browser during development. Here's how I did it.

The Problem

Once you have started your development Rails server and esbuild with the --watch option (if you used jsbundling-rails to set up, you probably use bin/dev), esbuild will recompile your assets upon change, but build errors will only be printed to the terminal. Your application won't complain about them -- instead it just delivers the files that were previously built and you might not notice something went wrong.

By default, esbuild logs errors to the terminal, which can be easy to miss when working with multiple tabs. Instead, I wanted a way for these errors to be displayed directly in the browser, making debugging more efficient.

The Solution

I created a system that:

  • Captures esbuild errors and writes them to a file.
  • Reads the error file in a Rails before-action.
  • Renders errors in the browser if present.

Step 1: Ignore Error Files in Git

Since esbuild errors will be logged to a file, we should prevent them from being committed. Add the following to .gitignore:

esbuild_error_*.txt

Step 2: Modify ApplicationController

To ensure errors are displayed only in development, I included a new concern in ApplicationController:

class ApplicationController < ActionController::Base
  include EsbuildErrorRendering if Rails.env.development?

Step 3: Create EsbuildErrorRendering Concern

This concern reads the error file and renders it in the browser. esbuild_error_rendering.rb

module EsbuildErrorRendering
  ESBUILD_ERROR = Rails.root.join("esbuild_error_#{Rails.env}.txt")

  def self.included(base)
    base.before_action :render_esbuild_error, if: :render_esbuild_error?
  end

  private

  def render_esbuild_error
    heading, errors = ESBUILD_ERROR.read.split("\n", 2)

    render html: <<~HTML.html_safe, layout: false
      <html>
        <head></head>
        <body>
          <h1>#{ERB::Util.html_escape(heading)}</h1>
          <pre>#{ERB::Util.html_escape(errors)}</pre>
        </body>
      </html>
    HTML
  end

  def render_esbuild_error?
    ESBUILD_ERROR.file? && !ESBUILD_ERROR.zero?
  end
end

Step 4: Configure Esbuild to Log Errors

To capture errors, I created esbuild.config.mjs and added a custom plugin:

import esbuild from 'esbuild'
import fs from 'fs'

const watch = process.argv.includes('--watch')
const errorLogFile = `esbuild_error_${process.env.RAILS_ENV}.txt`

const onEndPlugin = {
  name: 'onEnd',
  setup (build) {
    build.onEnd((result) => {
      if (result.errors.length > 0) {
        console.log(result.errors)
        const errors = result.errors.map(error => `${error.location.file}:${error.location.line}\n${error.text}`).join('\n\n')
        fs.writeFileSync(
          errorLogFile,
          `esbuild ended with ${result.errors.length} error${result.errors.length !== 1 ? 's' : ''}\n${errors}`
        )
      } else if (fs.existsSync(errorLogFile)) {
        fs.truncate(errorLogFile, 0, () => {})
      }
    })
  }
}

await esbuild
  .context({
    entryPoints: ['app/javascript/**/*.*'],
    bundle: true,
    sourcemap: true,
    format: 'esm',
    outdir: 'app/assets/builds',
    publicPath: '/assets',
    logLevel: 'info',
    plugins: [onEndPlugin]
  })
  .then(context => {
    if (watch) {
      context.watch()
    } else {
      context.rebuild().then(result => {
        context.dispose()
      })
    }
  }).catch(() => process.exit(1))

Step 5: Update package.json

Finally, I replaced the default esbuild script with my custom configuration:

"scripts": {
  "build": "node esbuild.config.mjs",

How Errors Appear in the Browser

With this setup, when an esbuild error occurs, it will be displayed directly in the browser instead of just the terminal. Below is an example of how an error message might look:

Esbuild Error

This makes debugging much faster by allowing you to immediately see the issue in the browser, rather than hunting for it in the terminal.

Final Thoughts

This setup improves error visibility by displaying esbuild errors directly in the browser, reducing the time spent switching between the terminal and browser. It also keeps concerns separate and integrates seamlessly with Rails' development environment.

If you're using esbuild with Rails, give this approach a try β€” your future self will thank you, and your browser will finally show some respect for esbuild errors.

πŸš€ Bonus Track: Rails Template for Easy Setup

If you want to enhance esbuild error handling in your Rails app without manually applying the changes, I’ve created a Rails app template that does all the setup for you!

This template:
βœ… Adds an esbuild error logging mechanism
βœ… Displays errors in the browser when esbuild fails
βœ… Creates the necessary files and configurations automatically

Instead of manually adding the concern and esbuild config, you can run this simple command:

rails app:template LOCATION='https://railsbytes.com/script/XnJsOq'

Make sure to:

  • Update your package.json to use node esbuild.config.mjs as the build command.
  • Adjust entryPoints, publicPath or outdir inside esbuild.config.mjs if needed.

With this, you'll have esbuild error handling up and running in seconds! 🎯
Check out the template here: RailsBytes Link

Subscribe to get future articles via the RSS feed .