Error Handling and Debugging
A comprehensive guide to handling errors gracefully and debugging BotUI applications effectively.
Common Error Scenarios
1. Bot Auto-Resolving (Hot Reload Issues)
Problem: Bot seems to skip actions and resolve automatically, especially in development.
Cause: Multiple bot instances or hot-reload triggering effects multiple times.
Solution:
import { useRef, useEffect } from 'react'
const MyBot = () => {
const botRef = useRef(null)
const hasStarted = useRef(false)
useEffect(() => {
if (!hasStarted.current) {
hasStarted.current = true
botRef.current = createBot()
startConversation(botRef.current)
}
}, [])
return (
<BotUI bot={botRef.current}>
<BotUIMessageList />
<BotUIAction />
</BotUI>
)
}
2. Plugin Errors Breaking Bot Flow
Problem: Plugin throws error and breaks the entire conversation.
Solution: Implement error boundaries in plugins:
const safePlugin = (block) => {
try {
// Your plugin logic
if (block.type === BOTUI_BLOCK_TYPES.MESSAGE) {
block.data.text = processText(block.data.text)
}
return block
} catch (error) {
console.error('Plugin error:', error, 'Block:', block)
// Add error indicator to meta
block.meta.pluginError = {
message: error.message,
timestamp: new Date().toISOString()
}
// Return original block to prevent breaking the flow
return block
}
}
myBot.use(safePlugin)
3. Custom Renderer Errors
Problem: Custom renderer components throw errors and crash the UI.
Solution: Use React Error Boundaries:
import React from 'react'
class BotUIErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
console.error('BotUI Error:', error, errorInfo)
// Log to your error reporting service
// errorReportingService.log(error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<div className="botui-error">
<p>Something went wrong with this message.</p>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.toString()}</pre>
</details>
</div>
)
}
return this.props.children
}
}
// Wrap your custom renderers
const SafeCustomRenderer = (props) => (
<BotUIErrorBoundary>
<CustomRenderer {...props} />
</BotUIErrorBoundary>
)
4. Promise Rejection Handling
Problem: Unhandled promise rejections in bot flow.
Solution: Always handle promise rejections:
const runBot = async (bot) => {
try {
await bot.message.add({ text: 'Starting...' })
const response = await bot.action.set(
{ placeholder: 'Enter something' },
{ actionType: 'input' }
)
// Process response
await bot.message.add({ text: `You entered: ${response.value}` })
} catch (error) {
console.error('Bot flow error:', error)
// Show error to user
await bot.message.add({
text: 'Sorry, something went wrong. Please try again.'
})
// Optionally restart or provide recovery options
await showErrorRecoveryOptions(bot)
}
}
const showErrorRecoveryOptions = async (bot) => {
const recovery = await bot.action.set(
{
options: [
{ label: 'Start over', value: 'restart' },
{ label: 'Contact support', value: 'support' },
{ label: 'Exit', value: 'exit' }
]
},
{ actionType: 'selectButtons' }
)
switch (recovery.selected.value) {
case 'restart':
await runBot(bot)
break
case 'support':
await bot.message.add({
text: 'Please contact support at [email protected]'
})
break
// exit case handles itself
}
}
Validation and Input Handling
User Input Validation
const ValidationAction = () => {
const bot = useBotUI()
const action = useBotUIAction()
const [value, setValue] = useState('')
const [errors, setErrors] = useState([])
const validators = {
required: (value) => value.trim() ? null : 'This field is required',
email: (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(value) ? null : 'Please enter a valid email'
},
minLength: (min) => (value) =>
value.length >= min ? null : `Minimum ${min} characters required`,
maxLength: (max) => (value) =>
value.length <= max ? null : `Maximum ${max} characters allowed`,
pattern: (regex, message) => (value) =>
regex.test(value) ? null : message
}
const validate = (inputValue) => {
const rules = action.meta.validation || {}
const newErrors = []
Object.entries(rules).forEach(([rule, config]) => {
const validator = validators[rule]
if (!validator) return
let error
if (typeof config === 'boolean' && config) {
error = validator(inputValue)
} else if (typeof config === 'object') {
error = validator(config.value)(inputValue)
} else {
error = validator(config)(inputValue)
}
if (error) {
newErrors.push(error)
}
})
setErrors(newErrors)
return newErrors.length === 0
}
const handleSubmit = () => {
if (validate(value)) {
bot.next({ value, isValid: true })
}
}
return (
<div className="validation-input">
<input
type={action.data.type || 'text'}
value={value}
onChange={(e) => {
setValue(e.target.value)
if (errors.length > 0) {
validate(e.target.value) // Re-validate on change if errors exist
}
}}
onBlur={() => validate(value)} // Validate on blur
placeholder={action.data.placeholder}
/>
<button onClick={handleSubmit} disabled={errors.length > 0}>
Continue
</button>
{errors.length > 0 && (
<div className="validation-errors">
{errors.map((error, index) => (
<div key={index} className="error">{error}</div>
))}
</div>
)}
</div>
)
}
Debugging Tools
1. Debug Plugin
const debugPlugin = (enabled = true) => (block) => {
if (!enabled) return block
console.group(`🤖 BotUI Debug - ${block.type.toUpperCase()}`)
console.log('Block Key:', block.key)
console.log('Type:', block.type)
console.log('Data:', block.data)
console.log('Meta:', block.meta)
console.log('Full Block:', block)
console.groupEnd()
return block
}
// Use in development
if (process.env.NODE_ENV === 'development') {
myBot.use(debugPlugin(true))
}
2. State Inspector
const StateInspector = () => {
const bot = useBotUI()
const action = useBotUIAction()
const messages = useBotUIMessage()
const [isOpen, setIsOpen] = useState(false)
if (process.env.NODE_ENV !== 'development') {
return null
}
return (
<div className="debug-inspector">
<button onClick={() => setIsOpen(!isOpen)}>
🔍 Debug Inspector
</button>
{isOpen && (
<div className="debug-panel">
<h4>Current Action</h4>
<pre>{JSON.stringify(action, null, 2)}</pre>
<h4>Messages ({messages.length})</h4>
<pre>{JSON.stringify(messages, null, 2)}</pre>
<h4>Bot Methods</h4>
<button onClick={() => console.log('Bot instance:', bot)}>
Log Bot to Console
</button>
</div>
)}
</div>
)
}
// Include in your app during development
<BotUI bot={myBot}>
<BotUIMessageList />
<BotUIAction />
<StateInspector />
</BotUI>
3. Conversation Logger
const ConversationLogger = () => {
const messages = useBotUIMessage()
useEffect(() => {
// Log conversation changes
console.log('Conversation updated:', {
messageCount: messages.length,
lastMessage: messages[messages.length - 1]
})
}, [messages])
const exportConversation = () => {
const conversation = {
timestamp: new Date().toISOString(),
messages: messages.map(msg => ({
type: msg.type,
data: msg.data,
meta: msg.meta,
key: msg.key
}))
}
const blob = new Blob([JSON.stringify(conversation, null, 2)], {
type: 'application/json'
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `conversation-${Date.now()}.json`
a.click()
}
if (process.env.NODE_ENV !== 'development') {
return null
}
return (
<button onClick={exportConversation}>
📥 Export Conversation
</button>
)
}
Performance Monitoring
1. Performance Plugin
const performancePlugin = (block) => {
const startTime = performance.now()
// Mark the start of block processing
performance.mark(`botui-block-${block.key}-start`)
// Add timing info to meta
block.meta.performance = {
processedAt: new Date().toISOString(),
startTime
}
// Measure after a microtask to capture processing time
setTimeout(() => {
const endTime = performance.now()
performance.mark(`botui-block-${block.key}-end`)
performance.measure(
`botui-block-${block.key}`,
`botui-block-${block.key}-start`,
`botui-block-${block.key}-end`
)
console.log(`Block ${block.key} processed in ${endTime - startTime}ms`)
}, 0)
return block
}
myBot.use(performancePlugin)
2. Memory Usage Monitor
const MemoryMonitor = () => {
const [memoryInfo, setMemoryInfo] = useState(null)
useEffect(() => {
const updateMemoryInfo = () => {
if (performance.memory) {
setMemoryInfo({
used: Math.round(performance.memory.usedJSHeapSize / 1048576), // MB
total: Math.round(performance.memory.totalJSHeapSize / 1048576), // MB
limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576) // MB
})
}
}
updateMemoryInfo()
const interval = setInterval(updateMemoryInfo, 5000)
return () => clearInterval(interval)
}, [])
if (!memoryInfo || process.env.NODE_ENV !== 'development') {
return null
}
return (
<div className="memory-monitor">
<small>
Memory: {memoryInfo.used}MB / {memoryInfo.total}MB
(Limit: {memoryInfo.limit}MB)
</small>
</div>
)
}
Error Recovery Patterns
1. Graceful Degradation
const GracefulRenderer = ({ message }) => {
try {
// Try to render complex component
return <ComplexCustomComponent data={message.data} />
} catch (error) {
console.warn('Complex renderer failed, falling back to simple:', error)
// Fallback to simple text rendering
return (
<div className="fallback-message">
<p>{message.data.text || 'Message could not be displayed'}</p>
{message.data.fallbackText && (
<p><em>{message.data.fallbackText}</em></p>
)}
</div>
)
}
}
2. Retry Mechanisms
const retryableAction = async (bot, actionData, actionMeta, maxRetries = 3) => {
let attempts = 0
while (attempts < maxRetries) {
try {
const result = await bot.action.set(actionData, actionMeta)
return result
} catch (error) {
attempts++
console.warn(`Action attempt ${attempts} failed:`, error)
if (attempts >= maxRetries) {
await bot.message.add({
text: 'Sorry, this action failed multiple times. Please try again later.'
})
throw error
}
await bot.message.add({
text: `Something went wrong. Retrying... (${attempts}/${maxRetries})`
})
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * attempts))
}
}
}
3. Health Checks
const HealthChecker = () => {
const bot = useBotUI()
const [health, setHealth] = useState('unknown')
const checkHealth = async () => {
try {
// Test basic bot functionality
await bot.message.getAll()
await bot.action.get()
setHealth('healthy')
} catch (error) {
console.error('Health check failed:', error)
setHealth('unhealthy')
}
}
useEffect(() => {
checkHealth()
const interval = setInterval(checkHealth, 30000) // Check every 30s
return () => clearInterval(interval)
}, [])
const handleReconnect = () => {
window.location.reload() // Simple recovery
}
if (health === 'unhealthy') {
return (
<div className="health-warning">
<p>⚠️ Bot connection lost</p>
<button onClick={handleReconnect}>Reconnect</button>
</div>
)
}
return null
}
Testing Error Scenarios
Unit Testing Error Conditions
import { createBot, BOTUI_BLOCK_TYPES } from 'botui'
describe('Error Handling', () => {
test('plugin errors should not break bot flow', async () => {
const bot = createBot()
// Add a plugin that throws
const faultyPlugin = (block) => {
if (block.data.text === 'trigger error') {
throw new Error('Test error')
}
return block
}
// Wrap with error handling
const safePlugin = (block) => {
try {
return faultyPlugin(block)
} catch (error) {
block.meta.hasError = true
return block
}
}
bot.use(safePlugin)
// This should not throw
await bot.message.add({ text: 'trigger error' })
const messages = await bot.message.getAll()
expect(messages[0].meta.hasError).toBe(true)
})
test('should handle invalid action data gracefully', async () => {
const bot = createBot()
// This should not crash the bot
await expect(bot.action.set(null, null)).rejects.toThrow()
// Bot should still be functional
await bot.message.add({ text: 'Recovery test' })
const messages = await bot.message.getAll()
expect(messages).toHaveLength(1)
})
})
Best Practices
- Always use error boundaries around custom renderers
- Implement graceful fallbacks for complex components
- Validate user input before processing
- Log errors but don't expose sensitive information
- Provide clear recovery options to users
- Test error scenarios in development
- Monitor performance in production
- Use debugging tools during development
By following these patterns, you can build robust BotUI applications that handle errors gracefully and provide excellent user experiences even when things go wrong.