Error Handling
Handle errors gracefully in your LightShell app.
All lightshell.* APIs are async and reject their promises on failure. This guide covers how to handle errors, what to expect from each API, and how to build resilient apps.
Basic Error Handling
Section titled “Basic Error Handling”Every LightShell API call should be wrapped in a try/catch block:
try { const content = await lightshell.fs.readFile('/path/to/file.txt') // use content} catch (err) { console.error('Failed to read file:', err.message)}The error object has a message property with a human-readable description of what went wrong.
Error Format
Section titled “Error Format”LightShell errors follow a structured, AI-friendly format:
LightShell Error [fs.readFile]: Permission denied -> Attempted to read: /etc/passwd -> Allowed read paths: $APP_DATA/**, $HOME/Documents/** -> To allow this path, update permissions.fs.read in lightshell.json -> Docs: https://lightshell.dev/docs/api/permissions#fsEach error includes:
- The API method that failed (
fs.readFile) - What was attempted
- What is currently allowed (for permission errors)
- A concrete fix
- A link to the relevant documentation
This format makes errors easy to diagnose manually and straightforward for AI assistants to fix automatically.
Common Errors by API
Section titled “Common Errors by API”File System Errors
Section titled “File System Errors”// File not foundtry { await lightshell.fs.readFile('/nonexistent/file.txt')} catch (err) { // err.message: "File not found: /nonexistent/file.txt"}
// Permission denied (restricted mode)try { await lightshell.fs.readFile('/etc/shadow')} catch (err) { // err.message: "LightShell Error [fs.readFile]: Permission denied ..."}
// Writing to a read-only locationtry { await lightshell.fs.writeFile('/usr/bin/test', 'data')} catch (err) { // err.message: OS-level permission error}
// Parent directory does not existtry { await lightshell.fs.writeFile('/tmp/nonexistent-dir/file.txt', 'data')} catch (err) { // Create the directory first, then retry await lightshell.fs.mkdir('/tmp/nonexistent-dir') await lightshell.fs.writeFile('/tmp/nonexistent-dir/file.txt', 'data')}Dialog Returns (Not Errors)
Section titled “Dialog Returns (Not Errors)”Dialog methods do not throw when the user cancels. They return null or false:
// open() returns null if the user cancelsconst filePath = await lightshell.dialog.open()if (filePath === null) { // User cancelled -- not an error, just do nothing return}
// save() returns null if cancelledconst savePath = await lightshell.dialog.save()if (!savePath) return
// confirm() returns false if cancelledconst confirmed = await lightshell.dialog.confirm('Delete?', 'Are you sure?')if (!confirmed) return
// prompt() returns null if cancelledconst input = await lightshell.dialog.prompt('Name', 'Enter your name:')if (input === null) returnA common mistake is wrapping dialog calls in try/catch expecting an error on cancel. The cancellation is not an error — always check the return value instead.
HTTP Errors
Section titled “HTTP Errors”try { const response = await lightshell.http.fetch('https://api.example.com/data')
// A non-2xx status is NOT an error -- the promise resolves with the status code if (response.status === 404) { console.log('Resource not found') return } if (response.status >= 400) { console.log('Server error:', response.status, response.body) return }
const data = JSON.parse(response.body)} catch (err) { // True errors: network failure, DNS resolution failure, timeout, permission denied console.error('Request failed:', err.message)}Note that HTTP status codes like 404 or 500 do not cause a rejection. The promise resolves with the response, and you check response.status yourself. The promise only rejects for actual failures like network errors, timeouts, or permission denials.
Process Execution Errors
Section titled “Process Execution Errors”try { const result = await lightshell.process.exec('git', ['status'])
// A non-zero exit code is NOT an error -- check result.code if (result.code !== 0) { console.error('git failed:', result.stderr) return }
console.log(result.stdout)} catch (err) { // True errors: command not found, permission denied, timeout console.error('Could not run command:', err.message)}Similar to HTTP, a command that runs but exits with a non-zero code is not an error from LightShell’s perspective. The promise resolves with { stdout, stderr, code }. Only check the code property.
Store Errors
Section titled “Store Errors”// get() returns null if the key does not exist -- not an errorconst value = await lightshell.store.get('nonexistent-key')// value === null
// has() returns false for missing keysconst exists = await lightshell.store.has('nonexistent-key')// exists === false
// delete() does not error if the key does not existawait lightshell.store.delete('nonexistent-key')// no errorThe store API is designed to be forgiving. Missing keys return null or false rather than throwing.
Global Error Handling
Section titled “Global Error Handling”Catch unhandled promise rejections across your entire app:
window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled error:', event.reason)
// Show a user-friendly error dialog lightshell.dialog.message( 'Something went wrong', event.reason?.message || String(event.reason), 'error' )
// Prevent the default browser behavior (console error) event.preventDefault()})This catches any rejected promise that does not have a .catch() or try/catch handler. It is a safety net, not a replacement for proper per-call error handling.
Global Error Handler for Synchronous Errors
Section titled “Global Error Handler for Synchronous Errors”window.addEventListener('error', (event) => { console.error('Runtime error:', event.message) lightshell.dialog.message('Error', event.message, 'error')})User-Friendly Error Dialogs
Section titled “User-Friendly Error Dialogs”Use lightshell.dialog.message to show errors in a native dialog instead of silently logging to the console:
async function loadFile(path) { try { const content = await lightshell.fs.readFile(path) return content } catch (err) { await lightshell.dialog.message( 'Could not open file', `Failed to read "${path}":\n${err.message}`, 'error' ) return null }}Error Reporting Pattern
Section titled “Error Reporting Pattern”For apps that need to report errors, build a helper function:
async function reportError(action, err) { const message = [ `Action: ${action}`, `Error: ${err.message}`, `Time: ${new Date().toISOString()}`, ].join('\n')
console.error(message)
await lightshell.dialog.message('Error', message, 'error')}
// Usagetry { await lightshell.fs.writeFile(path, data)} catch (err) { await reportError('Save file', err)}Defensive Patterns
Section titled “Defensive Patterns”Provide Fallback Defaults
Section titled “Provide Fallback Defaults”When loading settings or data, always fall back to defaults if the read fails:
async function loadSettings() { try { const raw = await lightshell.fs.readFile(settingsPath) return JSON.parse(raw) } catch { // File missing, corrupted, or permission denied -- use defaults return { theme: 'light', fontSize: 14, sidebarOpen: true } }}Retry Logic
Section titled “Retry Logic”For network operations that may fail transiently:
async function fetchWithRetry(url, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await lightshell.http.fetch(url) if (response.status >= 500 && attempt < maxRetries) { await new Promise(r => setTimeout(r, 1000 * attempt)) continue } return response } catch (err) { if (attempt === maxRetries) throw err await new Promise(r => setTimeout(r, 1000 * attempt)) } }}Safe JSON Parsing
Section titled “Safe JSON Parsing”Wrap JSON.parse when dealing with user data or file contents:
function safeParseJSON(str, fallback = null) { try { return JSON.parse(str) } catch { return fallback }}
const data = safeParseJSON(await lightshell.fs.readFile(path), [])Guard Against Null Dialog Returns
Section titled “Guard Against Null Dialog Returns”Always check dialog results before using them:
// Bad -- will crash if user cancelsconst path = await lightshell.dialog.open()const content = await lightshell.fs.readFile(path) // path is null!
// Goodconst path = await lightshell.dialog.open()if (!path) returnconst content = await lightshell.fs.readFile(path)Best Practices
Section titled “Best Practices”Wrap every fs call in try/catch. File operations are the most common source of runtime errors (missing files, permission issues, disk full).
Check dialog returns for null. Cancelled dialogs are normal user behavior, not errors. Never assume the user will always pick a file or confirm an action.
Distinguish error types for HTTP and process. Promise rejections mean the operation could not be performed at all. Non-2xx status codes and non-zero exit codes mean the operation ran but returned an unsuccessful result. Handle both.
Use a global error handler as a safety net. The unhandledrejection listener catches anything you missed. Log it and show a dialog so the user knows something went wrong.
Show errors in native dialogs, not just the console. Users cannot see the developer console. Use lightshell.dialog.message with 'error' type to surface problems visibly.
Provide defaults for everything. If loading saved data fails, fall back to sensible defaults. The app should always start, even if its data is missing or corrupted.