Troubleshooting Guide
This guide helps you diagnose and fix common issues when working with Opal-Vite.
Table of Contents
- Common Errors
- Controller Issues
- Compilation Issues
- Runtime Errors
- Build & Deployment Issues
- Performance Issues
- Debugging Techniques
Common Errors
Error: "function is not defined"
Symptom:
TypeError: ctrl.myFunction is not a functionCause: Function is called before it's defined in backtick JavaScript.
Solution: Define all helper functions BEFORE they are used:
# ❌ BAD
def connect
`
doSomething(); # Called before definition
function doSomething() {
// ...
}
`
end
# ✅ GOOD
def connect
`
function doSomething() {
// ...
}
doSomething(); # Called after definition
`
endExample from chart-app fix:
def connect
`
const ctrl = this;
// Define functions FIRST
ctrl.getDefaultData = function(type) { /* ... */ };
ctrl.getDefaultOptions = function(type) { /* ... */ };
// THEN use them
const data = ctrl.getDefaultData('line');
const options = ctrl.getDefaultOptions('line');
`
endError: "Missing target element"
Symptom:
Error: Missing target element "canvas" for "chart" controllerCause: Controller is instantiated multiple times or on elements without the target.
Solution: Ensure data-controller is on a parent element that contains all targets:
<!-- ❌ BAD: Controller on each button -->
<canvas data-controller="chart" data-chart-target="canvas"></canvas>
<button data-controller="chart" data-action="click->chart#update">Update</button>
<!-- ✅ GOOD: Controller on parent -->
<div data-controller="chart">
<canvas data-chart-target="canvas"></canvas>
<button data-action="click->chart#update">Update</button>
</div>Error: "Controller not found"
Symptom:
Stimulus: No controller found for identifier "my-controller"Cause: Controller not registered or naming mismatch.
Solutions:
- Check registration:
# application.rb
require 'controllers/my_controller'
StimulusController.register_all!- Check naming convention:
# File: my_controller.rb
class MyController < StimulusController # → my (not my_controller)
# HTML
<div data-controller="my"> # NOT my-controller or my_controller- Verify inheritance:
# ❌ BAD: Don't inherit from other custom controllers
class DataChartController < ChartController # Won't register!
# ✅ GOOD: Inherit from StimulusController
class ChartController < StimulusController # Will register!Error: "backtick_javascript required"
Symptom:
Opal compilation error: backtick_javascript is requiredCause: Inline JavaScript used without enabling backtick mode.
Solution: Add pragma at top of file:
# backtick_javascript: true
class MyController < StimulusController
def connect
`console.log('This works now!')`
end
endError: "Cannot find module"
Symptom:
Error: Cannot find module './my_file'
Error loading: LoadError: cannot load such file -- my_fileSolutions:
- Check file path:
# File structure:
# app/opal/
# application.rb
# utils/
# helper.rb
# ✅ CORRECT
require 'utils/helper'
# ❌ WRONG
require './utils/helper'
require 'helper'- Add explicit load path:
// vite.config.ts
export default defineConfig({
plugins: [
opal({
additionalLoadPaths: [
'./app/lib',
'./vendor/opal'
]
})
]
});- Check for typos:
# File: counter_controller.rb
require 'controllers/counter_controller' # ❌ Can't require itself!
require 'controllers/other_controller' # ✅ OKController Issues
Controllers Not Connecting
Symptom: connect() method never called, no console output.
Debugging steps:
- Check Stimulus is loaded:
// In browser console
console.log(window.Stimulus || window.application);- Check controller is registered:
// In browser console
const app = window.Stimulus || window.application;
console.log(app.router.modules);- Check HTML attributes:
<!-- All required attributes present? -->
<div data-controller="my"> <!-- controller name -->
<div data-my-target="element"> <!-- target -->
<button data-action="click->my#doSomething"> <!-- action -->- Check console for errors:
- Open DevTools (F12)
- Look for JavaScript/Opal errors
- Check Network tab for failed requests
Multiple Controller Instances
Symptom: Controller connects multiple times, state gets confused.
Cause: data-controller attribute on multiple elements.
Solution:
<!-- ❌ BAD: Creates 4 instances! -->
<div>
<button data-controller="chart" data-action="click->chart#update">
<button data-controller="chart" data-action="click->chart#add">
<button data-controller="chart" data-action="click->chart#remove">
<canvas data-controller="chart" data-chart-target="canvas">
</div>
<!-- ✅ GOOD: Creates 1 instance -->
<div data-controller="chart">
<button data-action="click->chart#update">
<button data-action="click->chart#add">
<button data-action="click->chart#remove">
<canvas data-chart-target="canvas">
</div>Values Not Updating
Symptom: Stimulus values don't trigger callbacks.
Solutions:
- Define value types:
class MyController < StimulusController
self.values = { count: :number } # Required!
end- Use correct value access:
# ❌ BAD
`this.count = 5` # Doesn't trigger callback
# ✅ GOOD
`this.countValue = 5` # Triggers countValueChanged- Implement callback:
def count_value_changed
`console.log('Count changed to:', this.countValue)`
endCompilation Issues
Opal Compilation Fails
Symptom:
Failed to compile Opal: SyntaxErrorCommon causes:
- Ruby syntax error:
# ❌ BAD
def my_method
puts "Missing end keyword"
# ✅ GOOD
def my_method
puts "Has end keyword"
end- Unsupported Ruby feature:
# ❌ BAD: Some Ruby features not supported in Opal
File.read('file.txt') # File I/O not available in browser
# ✅ GOOD: Use browser APIs
`fetch('/data.txt').then(r => r.text())`- Check Ruby version compatibility:
- Opal targets specific Ruby version (usually 3.2)
- Some Ruby 3.3+ features may not work
Gem Loading Issues
Symptom:
Error: Gem 'my-gem' requires server-side dependencies
LoadError: cannot load 'listen' (Sprockets dependency)Solutions:
- For Opal-compatible gems (with
opal/directory):
# Gemfile
gem 'inesita' # Has both lib/ and opal/ directories
# Works automatically - plugin prioritizes opal/ over lib/
require 'inesita' # Loads from opal/, avoiding server-side code- For gems without opal/ directory:
# Use only in server context or find browser-compatible alternative
# Gemfile (if using Rails)
group :development do
gem 'listen' # Server-only gem
end- Check gem has Opal support:
- Look for
opal/directory in gem - Check gem documentation for browser compatibility
- Alternative: Use equivalent JavaScript library
- Look for
Source Map Issues
Symptom: Can't debug Opal code, source maps broken.
Solution:
- Enable in Vite config:
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true
}
});- Check browser DevTools settings:
- Enable "Enable JavaScript source maps"
- Enable "Enable CSS source maps"
Runtime Errors
"Opal already loaded" Warning
Symptom:
Opal already loaded. Loading twice can cause troublesCause: Opal runtime loaded multiple times.
Solution: Usually harmless warning, but can indicate:
- Multiple entry points loading Opal:
// ❌ BAD: Don't import Opal multiple times
import '@opal-runtime'; // In file A
import '@opal-runtime'; // In file B
// ✅ GOOD: Import once in main entry
// application.js
import '../opal/application.rb'; // Opal loads automatically- Duplicate script tags:
<!-- ❌ BAD -->
<script src="/opal-runtime.js"></script>
<script src="/application.js"></script> <!-- Also loads Opal -->
<!-- ✅ GOOD -->
<script type="module" src="/application.js"></script>WebSocket Connection Fails
Symptom:
WebSocket connection to 'ws://localhost:3000/' failedSolutions:
- Check server is running:
# Start WebSocket server
node server.mjs- Check port matches:
# In controller
`const ws = new WebSocket('ws://localhost:3007')` # Must match server portCheck firewall:
- Allow WebSocket port
- Check corporate proxy/firewall
Use correct protocol:
# ❌ BAD: HTTP/HTTPS URL for WebSocket
`new WebSocket('http://localhost:3007')`
# ✅ GOOD: Use ws:// or wss://
`new WebSocket('ws://localhost:3007')`Chart.js Not Rendering
Symptom: Canvas exists but chart doesn't display.
Solutions:
- Check Chart.js is loaded:
// Browser console
console.log(window.Chart); // Should not be undefined- Check canvas size:
// Canvas needs explicit size
const canvas = document.querySelector('canvas');
console.log(canvas.width, canvas.height); // Should be > 0- Check chart instance created:
// After chart should be created
const canvas = document.querySelector('canvas');
console.log(canvas.chart || canvas.__chart); // Should exist- Destroy old chart before creating new:
def disconnect
`
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
`
endBuild & Deployment Issues
Build Fails in Production
Symptom:
vite build fails with errors not present in devSolutions:
- Check environment variables:
# Set production env vars
export NODE_ENV=production
export VITE_API_URL=https://api.example.com
vite build- Clear cache:
rm -rf node_modules/.vite
rm -rf dist
pnpm install
pnpm build- Check for dev-only code:
# ❌ BAD: Dev-only dependencies in production code
require 'pry' # Debug gem not available in production
# ✅ GOOD: Conditional requires
if ENV['RAILS_ENV'] == 'development'
require 'pry'
endLarge Bundle Size
Symptom: Built JavaScript file is very large (>1MB).
Solutions:
- Check what's included:
pnpm exec vite build --mode production
# Look at dist/ file sizes- Enable tree-shaking:
// vite.config.ts
export default defineConfig({
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log
drop_debugger: true // Remove debugger
}
}
}
});- Split chunks:
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['@hotwired/stimulus'],
charts: ['chart.js']
}
}
}
}
});- Lazy load large dependencies:
// Instead of
import Chart from 'chart.js';
// Use dynamic import
const Chart = await import('chart.js');Performance Issues
Slow HMR (Hot Module Replacement)
Symptom: Changes take long to reflect in browser.
Solutions:
- Reduce file watchers:
// vite.config.ts
export default defineConfig({
server: {
watch: {
ignored: ['**/node_modules/**', '**/dist/**']
}
}
});Use faster disk (SSD):
- Move project to SSD if on HDD
Reduce dependencies:
- Remove unused gems/packages
- Use lighter alternatives
Slow Page Load
Symptom: Page takes long to load/render.
Solutions:
Check bundle size (see above)
Lazy load routes:
# Load heavy controllers only when needed
def load_heavy_feature
`
import('./controllers/heavy_controller.rb').then(module => {
// Controller loaded
});
`
end- Optimize images/assets:
# Use compressed images
# Use modern formats (WebP, AVIF)
# Lazy load images- Use CDN for dependencies:
<!-- Load from CDN instead of bundling -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>Debugging Techniques
1. Browser DevTools
Check Console:
// Enable verbose logging
localStorage.debug = '*'; // All logs
localStorage.debug = 'stimulus*'; // Only Stimulus logsCheck Network:
- Look for failed requests (404, 500)
- Check request/response headers
- Verify correct Content-Type
Check Elements:
- Inspect Stimulus data attributes
- Verify DOM structure matches expectations
- Check computed styles
2. Opal Debugging
Add debug output:
def my_method
puts "Debug: my_method called" # Shows in browser console
puts "Value: #{@value.inspect}"
`console.log('JavaScript value:', someJsVariable)`
endInspect Opal objects:
def connect
# Print all instance variables
instance_variables.each do |var|
puts "#{var}: #{instance_variable_get(var).inspect}"
end
end3. Stimulus Debugging
Log all controller lifecycles:
class MyController < StimulusController
def connect
puts "#{self.class.name} connected"
`console.log('Element:', this.element)`
`console.log('Targets:', this.targets)`
end
def disconnect
puts "#{self.class.name} disconnected"
end
endInspect controller state:
// Browser console
const el = document.querySelector('[data-controller="my"]');
const app = window.Stimulus || window.application;
const controller = app.getControllerForElementAndIdentifier(el, 'my');
console.log('Controller:', controller);
console.log('Targets:', controller.targets);
console.log('Values:', controller.valueDescriptorMap);4. Network Debugging
Proxy requests:
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('Proxying:', req.method, req.url);
});
}
}
}
}
});Mock API responses:
def fetch_data
`
// Mock during development
if (import.meta.env.DEV) {
return Promise.resolve({ data: 'mock' });
}
return fetch('/api/data').then(r => r.json());
`
end5. Time Travel Debugging
Record controller state:
class MyController < StimulusController
def connect
`
this.stateHistory = [];
this.recordState = () => {
this.stateHistory.push({
timestamp: Date.now(),
count: this.countValue,
// ... other state
});
};
`
end
def increment
`
this.countValue++;
this.recordState();
`
end
endGetting Help
If you're still stuck:
Check existing issues:
Create minimal reproduction:
- Isolate the problem
- Create smallest possible example
- Share code on GitHub/CodeSandbox
Provide context:
- Opal-Vite version
- Node.js version
- Ruby version (if using gems)
- Browser version
- Operating system
- Full error message with stack trace
Community resources:
Additional Resources
- TESTING.md - Testing strategies
- MIGRATION.md - Migration guide
- examples/ - Working examples
- Opal Documentation
- Vite Documentation
- Stimulus Handbook
