Package Exports
- @visulima/ono
- @visulima/ono/package.json
- @visulima/ono/page/context
- @visulima/ono/server/open-in-editor
Readme
Visulima ono (Oh No!)
A modern, delightful error overlay and inspector for Node.js servers and dev tooling.
Daniel Bannert's open source work is supported by the community on GitHub Sponsors
| Light Mode | Dark Mode |
|---|---|
![]() |
![]() |
Install
pnpm add @visulima/ononpm i @visulima/onoyarn add @visulima/onoFeatures
- Pretty, theme‑aware error page
- Sticky header (shows error name/message while scrolling)
- One‑click copy for error title (icon feedback)
- Stack trace viewer
- Shiki‑powered syntax highlighting (singleton highlighter)
- Tabs for frames; grouping for internal/node_modules/application frames
- Tooltips and labels to guide usage
- Optional "Open in editor" button per frame
- Error causes viewer (nested causes, each with its own viewer)
- Solutions panel
- Default open; smooth expand/collapse without layout jump
- Animated height/opacity; icon toggles open/close
- Built-in rule-based Markdown hints for common issues (ESM/CJS interop, export mismatch, port in use, missing files/case, TS path mapping, DNS/connection, React hydration mismatch, undefined property access)
- Custom solution finders support
- Raw stack trace panel
- Theme toggle (auto/dark/light) with persistence
- Copy to Clipboard - One-click copying for all data sections
- Responsive Design - Sticky sidebar navigation with smooth scrolling
- Consistent tooltips (one global script; components only output HTML)
New in latest version:
- Tabbed Interface - Switch between Stack and Context views
- Request Context Panel - Detailed HTTP request debugging information
- cURL command with proper formatting and copy functionality
- Headers, cookies, body, and session data
- App routing, client info, Git status, and version details
- Smart data sanitization and masking for sensitive information
- Flexible Context API - Add any custom context data via
createRequestContextPage()
- Modern Ono Class API - Simple, consistent interface for both HTML and ANSI rendering
- Solution Finders - Extensible system for custom error solutions
Accessibility and keyboard UX
- ARIA-correct tabs and panels for stack frames; improved labeling
- Focus trap within the overlay; restores focus on close
- Keyboard shortcuts help dialog (press Shift+/ or “?” button)
- Buttons/controls are keyboard-activatable (Enter/Space)
Editor integration
- Editor selector is always visible; selection persists (localStorage)
- Uses server endpoint when configured; otherwise opens via editor URL scheme (defaults to VS Code)
Using the Ono class (recommended)
The new Ono class provides a simple, consistent API for both HTML and ANSI error rendering:
import { Ono } from "@visulima/ono";
const ono = new Ono();
// HTML error page
const html = await ono.toHTML(error, {
cspNonce: "your-nonce",
theme: "dark",
solutionFinders: [
/* custom finders */
],
});
// ANSI terminal output
const { errorAnsi, solutionBox } = await ono.toANSI(error, {
solutionFinders: [
/* custom finders */
],
});Node.js HTTP Server Example
import { createServer } from "node:http";
import { Ono } from "@visulima/ono";
import createRequestContextPage from "@visulima/ono/page/context";
import { createNodeHttpHandler } from "@visulima/ono/server/open-in-editor";
const ono = new Ono();
const openInEditorHandler = createNodeHttpHandler();
const server = createServer(async (request, response) => {
const url = new URL(request.url || "/", `http://localhost:3000`);
// Open-in-editor endpoint
if (url.pathname === "/__open-in-editor") {
return openInEditorHandler(request, response);
}
try {
// Your app logic here
throw new Error("Something went wrong!");
} catch (error) {
// Create context page with request information
const contextPage = await createRequestContextPage(request, {
context: {
request: {
method: request.method,
url: request.url,
headers: request.headers,
},
user: {
client: {
ip: request.socket?.remoteAddress,
userAgent: request.headers["user-agent"],
},
},
},
});
// Generate HTML error page
const html = await ono.toHTML(error, {
content: [contextPage],
openInEditorUrl: "__open-in-editor",
cspNonce: "nonce-" + Date.now(),
theme: "auto",
});
response.writeHead(500, {
"Content-Type": "text/html",
"Content-Length": Buffer.byteLength(html, "utf8"),
});
response.end(html);
}
});
server.listen(3000);Hono Framework Example
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { Ono } from "@visulima/ono";
import createRequestContextPage from "@visulima/ono/page/context";
const app = new Hono();
const ono = new Ono();
app.get("/", (c) => c.text("OK"));
app.get("/error", () => {
throw new Error("Boom from Hono");
});
app.onError(async (err, c) => {
const contextPage = await createRequestContextPage(c.req.raw, {
context: {
request: {
method: c.req.method,
url: c.req.url,
headers: Object.fromEntries(c.req.raw.headers.entries()),
},
},
});
const html = await ono.toHTML(err, {
content: [contextPage],
cspNonce: "hono-nonce-" + Date.now(),
theme: "dark",
});
return c.html(html, 500);
});
serve({ fetch: app.fetch, port: 3000 });API
Ono Class
The main API for rendering errors in both HTML and ANSI formats.
Constructor
const ono = new Ono();Methods
toHTML(error, options?) => Promise<string>
Renders an error as an HTML page.
- error:
unknown- The error to render - options:
TemplateOptions(optional)content?: ContentPage[]- Additional pages to display as tabscspNonce?: string- CSP nonce for inline scripts/styleseditor?: Editors- Default editor for "Open in editor" functionalityopenInEditorUrl?: string- Server endpoint for opening files in editorsolutionFinders?: SolutionFinder[]- Custom solution finderstheme?: 'dark' | 'light' | 'auto'- Theme preference
Returns the complete HTML string for the error page.
toANSI(error, options?) => Promise<{ errorAnsi: string; solutionBox?: string }>
Renders an error as ANSI terminal output.
- error:
unknown- The error to render - options:
CliOptions(optional)solutionFinders?: SolutionFinder[]- Custom solution finders- All other options from
@visulima/errorrenderError options
Returns an object with errorAnsi (the formatted error) and optional solutionBox (suggested solutions).
createRequestContextPage(request, options) => Promise<ContentPage | undefined>
Creates a context page with detailed request debugging information.
- request:
Request- The HTTP request object - options:
ContextContentOptionscontext?: Record<string, unknown>- Additional context dataheaderAllowlist?: string[]- Headers to include (default: all)headerDenylist?: string[]- Headers to excludemaskValue?: string- Mask for sensitive values (default: "[masked]")
createNodeHttpHandler(options) => (req, res) => void
Creates an HTTP handler for opening files in editors.
- options:
OpenInEditorOptions(optional)projectRoot?: string- Project root directoryallowOutsideProject?: boolean- Allow opening files outside project
Returns an Express/Node.js compatible request handler.
CLI Example
import { Ono } from "@visulima/ono";
const ono = new Ono();
try {
throw new Error("Something went wrong!");
} catch (error) {
// Basic ANSI output
const result = await ono.toANSI(error);
console.log(result.errorAnsi);
if (result.solutionBox) {
console.log("\n" + result.solutionBox);
}
// With custom solution finder
const resultWithCustom = await ono.toANSI(error, {
solutionFinders: [
{
name: "custom-finder",
priority: 100,
handle: async (err, context) => ({
header: "Custom Solution",
body: "Try checking your configuration.",
}),
},
],
});
}Request Context Panel
Use createRequestContextPage() to create a "Context" tab with comprehensive debugging information:
- Request Overview - cURL command with proper formatting and copy functionality
- Headers - HTTP headers with smart masking for sensitive data
- Body - Request body content with proper formatting
- Session - Session data in organized key-value tables
- Cookies - Cookie information in readable format
- Dynamic Context Sections - Any additional context keys you provide are automatically rendered as sections with:
- Proper titles (capitalized)
- Copy buttons for JSON data
- Organized key-value tables
- Sticky sidebar navigation
Built-in sections (when data is provided):
app- Application routing details (route, params, query)user- Client information (IP, User-Agent, geo)git- Repository status (branch, commit, tag, dirty state)versions- Package versions and dependencies
Custom sections - Add any context data you want:
database- Database connection info, queries, etc.cache- Cache status and keysenvironment- Environment variablesperformance- Performance metrics- And more!
Deep Object & Array Support - The context panel intelligently renders:
- Nested objects with proper indentation and visual hierarchy
- Arrays with indexed items and collapsible structure
- Complex data types (strings, numbers, booleans, null, undefined)
- Performance-optimized rendering with depth limits (max 3 levels)
- Smart truncation for large datasets (shows first 10 items/keys)
All sections include copy buttons for easy data extraction and debugging.
Custom Solution Finders
Create custom solution finders to provide specific guidance for your application's errors:
import { Ono } from "@visulima/ono";
const customFinder = {
name: "my-app-finder",
priority: 100, // Higher priority = checked first
handle: async (error, context) => {
if (error.message.includes("database connection")) {
return {
header: "Database Connection Issue",
body: "Check your database configuration and ensure the server is running.",
};
}
if (error.message.includes("authentication")) {
return {
header: "Authentication Error",
body: "Verify your API keys and authentication tokens are valid.",
};
}
return undefined; // No solution found
},
};
const ono = new Ono();
const html = await ono.toHTML(error, {
solutionFinders: [customFinder],
});Copy to Clipboard
All data sections in the Request Context Panel include copy buttons that:
- Copy data in JSON format for easy debugging
- Provide visual feedback (button changes to "Copied!" with green styling)
- Support both modern
navigator.clipboardAPI and fallback methods - Work across all browsers and environments
Adding custom pages/tabs via options.content
You can add any number of custom pages using the content option:
import { Ono } from "@visulima/ono";
import createRequestContextPage from "@visulima/ono/page/context";
const ono = new Ono();
// Create a context page with request information
const contextPage = await createRequestContextPage(request, {
context: {
request: request,
app: { routing: { route: "/api/users", params: {}, query: {} } },
user: { client: { ip: "127.0.0.1", userAgent: "Mozilla/5.0..." } },
database: { connection: "active", queries: ["SELECT * FROM users"] },
},
});
// Add custom pages
const customPages = [
contextPage, // Context page with request info
{
id: "performance",
name: "Performance",
code: {
html: "<div><h3>Performance Metrics</h3><p>Custom performance data here...</p></div>",
},
},
{
id: "debug",
name: "Debug Info",
code: {
html: "<div><h3>Debug Information</h3><pre>" + JSON.stringify(debugData, null, 2) + "</pre></div>",
},
},
];
const html = await ono.toHTML(error, {
content: customPages,
cspNonce: "your-nonce",
});Examples
The examples/ directory contains working examples for different use cases:
CLI Example (examples/cli/)
Demonstrates basic ANSI output and custom solution finders:
cd examples/cli
node index.jsNode.js HTTP Server (examples/node/)
Complete HTTP server example with rich context pages:
cd examples/node
node index.jsTry these routes:
/error- Basic error with context/esm-cjs- ESM/CJS interop error/export-mismatch- Export mismatch error/custom-solution- Custom solution finder demo
Hono Framework (examples/hono/)
Hono framework integration example:
cd examples/hono
node index.jsTry these routes:
/error- Basic error handling/error-html- HTML error page/api/error-json- JSON error response
Server helpers
From @visulima/ono/server/open-in-editor:
openInEditor(request, options)— core function (usesopen-editorunder the hood)createNodeHttpHandler(options)— returns(req, res) => voidfor Node http serverscreateExpressHandler(options)— returns(req, res) => voidfor Express/Connect
Options:
projectRoot?: string— defaults toprocess.cwd()allowOutsideProject?: boolean— defaults tofalse
Editor selector
- Always visible
- Persists user choice in
localStorage(ono:editor) - Used for both the server opener (sent as
editorin the POST body) and the client-side fallback
Client-side fallback editor links
- If
openInEditorUrlis not set, clicking “Open in editor” uses editor URL schemes on the client. The default editor is VS Code. The selected editor in the header is respected. - Supported editors and templates (placeholders:
%f= file,%l= line,%c= column when supported):- textmate:
txmt://open?url=file://%f&line=%l - macvim:
mvim://open?url=file://%f&line=%l - emacs:
emacs://open?url=file://%f&line=%l - sublime:
subl://open?url=file://%f&line=%l - phpstorm:
phpstorm://open?file=%f&line=%l - atom:
atom://core/open/file?filename=%f&line=%l - atom-beta:
atom-beta://core/open/file?filename=%f&line=%l - brackets:
brackets://open?url=file://%f&line=%l - clion:
clion://open?file=%f&line=%l - code (VS Code):
vscode://file/%f:%l:%c - code-insiders:
vscode-insiders://file/%f:%l:%c - codium (VSCodium):
vscodium://file/%f:%l:%c - cursor:
cursor://file/%f:%l:%c - emacs:
emacs://open?url=file://%f&line=%l - idea:
idea://open?file=%f&line=%l - intellij:
idea://open?file=%f&line=%l - macvim:
mvim://open?url=file://%f&line=%l - notepad++:
notepad-plus-plus://open?file=%f&line=%l - phpstorm:
phpstorm://open?file=%f&line=%l - pycharm:
pycharm://open?file=%f&line=%l - rider:
rider://open?file=%f&line=%l - rubymine:
rubymine://open?file=%f&line=%l - sublime:
subl://open?url=file://%f&line=%l - textmate:
txmt://open?url=file://%f&line=%l - vim:
vim://open?url=file://%f&line=%l - visualstudio:
visualstudio://open?file=%f&line=%l - vscode:
vscode://file/%f:%l:%c - vscodium:
vscodium://file/%f:%l:%c - webstorm:
webstorm://open?file=%f&line=%l - xcode:
xcode://open?file=%f&line=%l - zed:
zed://open?file=%f&line=%l&column=%c - android-studio:
idea://open?file=%f&line=%l
- textmate:
Keyboard Shortcuts
- Shift+/ (or ?) — Open shortcuts help dialog
- Esc — Close dialogs
- Enter/Space — Activate focused control (e.g., toggles, tabs)
Tooltips
Components emit HTML with data-tooltip-trigger; a single script exported by the tooltip module is imported once by the layout (so there's no duplication).
Extend the VisulimaError
import { VisulimaError } from "@visulima/error";
class MyError extends VisulimaError {
constructor(message: string) {
super({
name: "MyError",
message,
});
}
}
throw new MyError("My error message");
// or
const error = new MyError("My error message");
error.hint = "My error hint";
throw error;Pretty code frame
import { codeFrame } from "@visulima/error";
const source = "const x = 10;\nconst error = x.y;\n";
const loc = { column: 16, line: 2 };
const frame = codeFrame(source, loc);
console.log(frame);
// 1 | const x = 10;
// > 2 | const error = x.y;
// | ^Supported Node.js Versions
Libraries in this ecosystem make the best effort to track Node.js’ release schedule. Here’s a post on why we think this is important.
Contributing
If you would like to help take a look at the list of issues and check our Contributing guild.
Note: please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
Credits
License
The visulima error is open-sourced software licensed under the MIT

