Use native Error for local failures. Use @vercel/error when callers need stable codes, clients need approved text, or readers need recovery advice.
The package separates scope and code from developer details and public text approved for the recipient. It also formats terminal output and has no runtime dependencies.
pnpm add @vercel/errorDirect Node.js execution requires Node.js 24 or newer. The package is ESM-only and has no CommonJS export.
import { VercelError } from '@vercel/error';
import { errorResponse } from '@vercel/error/server';
export async function handleCheckout(
checkoutConnection: () => Promise<void>,
): Promise<Response> {
try {
await checkoutConnection();
return new Response(null, { status: 204 });
} catch (cause) {
const error = new VercelError('Database connection checkout failed', {
cause,
code: 'checkout_failed',
scope: 'database',
statusCode: 500,
public: {
message: 'We could not complete the request.',
},
});
const result = errorResponse(error);
return new Response(result.body, result);
}
}cause keeps the original failure. The constructor message is for developers. Only public supplies the response message. scope and code also reach the client, and statusCode becomes the HTTP status.
Read the project narrative for the problem, alternatives, and adoption path.
The optional vercel-error skill helps coding agents choose fields, migrate existing errors, and review which details reach clients:
npx skills add vercel-labs/error --skill vercel-errorInstalling the npm package does not activate the skill.
| Import | Exports |
|---|---|
@vercel/error |
VercelError, createErrors, guards, extractors, VercelErrorLike, RecognizedVercelError, and shared types |
@vercel/error/client |
fromHttpResponse, parseErrorResponseData, fromErrorResponseData, ErrorResponseData, and option types |
@vercel/error/server |
buildErrorResponseData, errorResponse, wantsAnsi, ErrorResponseDataInput, ErrorResponseData, ErrorResponseInput, ErrorResponse, ErrorResponseOptions, HeadersLike |
@vercel/error/format |
formatError, frame, hint, fix, link, format and section types |
scope and code identify an error for software. When set, each must contain more than whitespace. Validation preserves the original text, including any surrounding spaces.
Branch on these fields, not on messages or terminal output. The same rule applies to parsed responses.
if (hasCode(error, 'pool_exhausted')) {
// Apply an application-owned policy.
}Keep codes stable when wording changes. A scope is useful when it names the service, package, or subsystem that owns the code.
Both fields are sent to clients. A code like admin_key_invalid reveals that the resource exists; where that matters, return a generic scope and code (or none) for protected resources.
The constructor message and optional reason, hint, fix, and link are developer-facing diagnostics. They are readonly after construction and appear in toString() and diagnostic toJSON() output.
| Field | Meaning |
|---|---|
message |
What failed |
reason |
Why it failed, when the cause is known |
hint |
Advisory information that may help investigation |
fix |
A known remediation and its required action |
link |
Documentation for the technical failure |
Preserve an original failure through cause. Put nested debugging context in metadata and flat telemetry values in attributes.
VercelErrorLike describes error fields without requiring the package tag. Formatters and callers inspecting fields can use structural values.
RecognizedVercelError adds the existing package marker and is accepted by response producers. VercelError instances and subclasses satisfy it, as do factory results whose custom constructors use current declarations.
import {
isVercelError,
VercelError,
type VercelErrorLike,
} from '@vercel/error';
import { formatError } from '@vercel/error/format';
import { buildErrorResponseData, errorResponse } from '@vercel/error/server';
const error = new VercelError('GitHub issue lookup failed', {
public: { message: 'We could not retrieve the GitHub issue.' },
});
const fields: VercelErrorLike = error;
formatError(fields);
if (isVercelError(fields)) {
const data = buildErrorResponseData(fields);
const response = errorResponse(fields);
}isVercelError() narrows unknown values to RecognizedVercelError. VercelErrorLike does not carry the package marker in its static type, so keep a concrete error type or call the guard before producing a response.
Recognition does not provide class methods or validate diagnostic contents. It also does not authenticate the producer or approve disclosure.
Version 0.6.0 adds the recognized-error requirement to the producer types. Version 0.5.0 allows a plain Error call in TypeScript, then rejects it at runtime.
For migration guidance, see Core errors and the allowlisted logging reference.
Both producers take authored response prose from nested public details. public.message must be nonblank. Optional reason, hint, fix, and link fields must be strings; blank values are omitted and other text is preserved.
The constructor throws TypeError for invalid details. It drops unknown fields and freezes a copy, so later input changes cannot alter the approved text.
interface PublicErrorDetails {
readonly message: string;
readonly reason?: string;
readonly hint?: string;
readonly fix?: string;
readonly link?: string;
}Without public, a recognized error sends An error occurred. instead of developer text. Explicit input requires public. docsBaseUrl sets only the developer link; set public.link explicitly for clients. scope, code, and HTTP status still reach clients.
statusCode is the HTTP status this error should get if it becomes an HTTP response. errorResponse() validates it at serialization: integers 400 through 599 only; omission defaults to 500. The finished response uses the concrete property status.
statusCode is an HTTP category, not application identity. Several codes can share one status.
requestId, metadata, and attributes remain mutable so request and telemetry boundaries can add context after construction. They are excluded from response data and both HTTP body formats.
VercelError#toJSON() is diagnostic serialization. It includes the developer message, stack, public details, metadata, attributes, and other defined enumerable fields; it excludes cause. Its output may contain sensitive data. Use buildErrorResponseData() for client-safe data or errorResponse() for HTTP serialization. Logs need their own disclosure policy.
createErrors creates a typed family with shared scope, documentation, metadata, attributes, reporting, or a custom class.
import { createErrors } from '@vercel/error';
type DatabaseCode = 'connection_failed' | 'pool_exhausted' | 'timeout';
const errors = createErrors<DatabaseCode>({
scope: 'database',
docsBaseUrl: 'https://example.com/internal/errors/database',
attributes: { 'service.name': 'database-api' },
onReport: (error) => {
sentry.captureException(error);
},
});Pass a whole error to a reporter only when its policy permits all included fields. For restricted logs, use the allowlisted logging recipe.
The factory always returns three methods:
| Method | Behavior |
|---|---|
create(message, options?) |
Create and return an error |
raise(message, options?) |
Create and throw |
report(message, options?) |
Create, call onReport, then return the same instance |
Without onReport, report() formats a sanitized terminal frame and passes that string to console.error. The default output omits raw stack inspection and enumerable diagnostics. create() and raise() never report, which leaves the operation boundary responsible for recording thrown errors once.
onReport receives the original error, is synchronous, and returns undefined. A synchronous callback exception propagates and replaces the error that report() would have returned. Async callbacks are rejected by TypeScript. The callback may expose PII or confidential developer prose; reporter integrations must allowlist or scrub data before transmission.
Factory and per-error metadata and attributes merge one level deep, with per-error keys winning. A per-error link also takes precedence over docsBaseUrl.
Pass ErrorClass to infer a custom subtype:
import { VercelError, createErrors } from '@vercel/error';
import type { VercelErrorOptions } from '@vercel/error';
class DatabaseError extends VercelError<'pool_exhausted'> {
readonly retryable = true;
constructor(
message: string,
options: VercelErrorOptions<'pool_exhausted'> = {},
) {
super(message, options);
this.name = 'DatabaseError';
}
}
const errors = createErrors({
ErrorClass: DatabaseError,
scope: 'database',
});
errors.create('Pool exhausted', { code: 'pool_exhausted' }).retryable;A custom TError type argument requires its matching ErrorClass. Omitting ErrorClass always returns the base VercelError<TCode> type and instance.
VercelError#toString() delegates to formatError(error, { format: 'auto' }). Import formatError from @vercel/error/format when a caller needs an explicit preset.
| Preset | Tree connectors | ANSI color | Reads ambient state |
|---|---|---|---|
auto |
Detected | Detected | Yes |
plain |
No | No | No |
tree |
Yes | No | No |
ansi |
Yes | Yes | No |
auto checks NO_COLOR, then FORCE_COLOR, then TTY support. NO_COLOR selects tree output without ANSI; otherwise, FORCE_COLOR or TTY support selects ANSI tree output. All other environments use plain output. Explicit presets are deterministic; ansi produces ANSI output even when the server is not a TTY.
import { formatError } from '@vercel/error/format';
const text = formatError(error, { format: 'plain' });Formatted output follows this structure:
error: VercelError [database:pool_exhausted] Database pool exhausted
│
├── All connections are in use.
├─▸ hint: Inspect connection checkout duration.
├─▸ fix: Release leaked connections.
╰─▸ read more: https://example.com/internal/database/pool-exhausted
hint, fix, and link return structured FrameSection values. frame uses those tokens to select labels, connectors, and color. A raw string such as hint: text remains an ordinary detail; the renderer does not parse prefixes to infer meaning.
import { fix, frame, hint, link } from '@vercel/error/format';
const output = frame(
'Build failed: missing entry point',
[
'No index.ts or index.js was found.',
hint('Check the configured source directory.'),
fix('Create src/index.ts or update the package entry.'),
link('https://vercel.com/docs/builds'),
],
{ format: 'tree' },
);All caller-controlled text is sanitized. CRLF becomes LF, bare carriage returns are removed, and every extra line of multiline input is prefixed by the frame's own connector or indentation, so input text can't fake a frame line. Tabs, blank lines, and multiline content remain readable.
errorResponse returns a framework-neutral ErrorResponse with { status, body, headers }:
import { errorResponse } from '@vercel/error/server';
const result = errorResponse(error);
return new Response(result.body, result);errorResponse() throws TypeError on a plain Error rather than guessing its message is safe to publish. Wrap it instead: new VercelError('…', { cause: err, public: { message: '…' } }).
The JSON body uses the same { "error": { "code", "message", … } } envelope as the Vercel REST API, not RFC 9457 problem details. If a consumer needs application/problem+json, map code (with link) to type, message to detail, and the remaining fields to extension members.
JSON is the default body. To let the request choose between JSON and ANSI text, pass the request:
const result = errorResponse(error, {
request,
onSerialize: (source, context) => {
recordSerialization(source, context);
},
});request accepts a Request or HeadersLike. The headers choose the format in this order:
X-Error-Formatwins when present. Only the exact valueansiselects ANSI; any other value selects JSON.- Otherwise, check exact
text/plain+ansientries inAccept. A validq > 0selects ANSI; missingqcounts as1. Media type andqnames ignore case. Duplicates use the highest validq. - Unsupported parameters, including a non-UTF-8 charset, cannot select ANSI. If exact entries exist but none qualifies, use JSON without checking
User-Agent. - If no exact entry exists, a case-sensitive
curl/inUser-Agentselects ANSI. Wildcards do not select ANSI.
Headers choose a format; they do not authenticate the caller or authorize disclosure.
JSON uses Content-Type: application/json. ANSI uses Content-Type: text/plain+ansi; charset=utf-8. When you supply request, both include Vary: X-Error-Format, Accept, User-Agent. Without request, Vary is omitted.
Both formats use the same public details. ANSI never exposes the developer message, reason, hint, fix, or link.
onSerialize runs after the response is built. It receives the original source and { status, bodyFormat }, where bodyFormat is json or ansi. The callback is synchronous, returns undefined, and propagates exceptions.
Untagged input must place approved prose under public:
const result = errorResponse({
scope: 'api',
code: 'rate_limited',
statusCode: 429,
public: {
message: 'Too many requests.',
hint: 'Wait before retrying.',
},
});The input uses statusCode; the result uses status. A top-level message never substitutes for public.message.
The completed server result is:
interface ErrorResponse {
readonly status: number;
readonly body: string;
readonly headers: Record<string, string>;
}Use buildErrorResponseData() when you need error data for a tool result or message.
- Accepts a recognized error, including a local
VercelErroror a value narrowed byisVercelError(), or explicitErrorResponseDataInput. - Returns
scope,code, and approvedpublictext. Developer details, diagnostics, and HTTP status are excluded. - Works in browsers, workers, edge runtimes, and Node.
A coding agent calls getGitHubIssue to read an issue before fixing a bug. This example builds the tool's error result when GitHub returns HTTP 503, using unavailable as the error code:
import { buildErrorResponseData } from '@vercel/error/server';
const data = buildErrorResponseData({
scope: 'github',
code: 'unavailable',
public: { message: 'We could not retrieve the GitHub issue.' },
});
const toolResult = { success: false, ...data };The agent receives this tool result:
{
"success": false,
"error": {
"scope": "github",
"code": "unavailable",
"message": "We could not retrieve the GitHub issue."
}
}The tool adds success: false; the builder supplies error. Adapt the wrapper to your tool's schema. For an existing message field, use data.error.message.
See the data producer reference for validation, fallback messages, and HTTP status rules.
fromHttpResponse() consumes a Web Response when it has an error status, an application/json content type, and valid ErrorResponseData. It derives statusCode from the response and reads x-vercel-id into requestId by default.
import { fromHttpResponse } from '@vercel/error/client';
const response = await fetch(url);
if (!response.ok) {
const failedFetch = new Error(`${response.url} returned ${response.status}`);
const error = await fromHttpResponse(response, { cause: failedFetch });
sentry.captureException(error ?? failedFetch);
throw error ?? failedFetch;
}An explicit requestId overrides the response header. Set requestIdHeader to another header name for an application-owned correlation ID, or to false to skip header lookup. x-vercel-id is Vercel routing context, not a promise that the value matches an application trace ID. For a cross-origin browser request, the server must include x-vercel-id in Access-Control-Expose-Headers before client code can read it.
The function returns undefined for a non-error status, a different content type, malformed JSON, or invalid response data. It does not report the error or create a fallback. It consumes the body after status and content type match; clone the response first if another caller also needs the body.
Use parseErrorResponseData() and fromErrorResponseData() when the input is already decoded or came through another serialization channel:
import {
fromErrorResponseData,
parseErrorResponseData,
} from '@vercel/error/client';
const data = parseErrorResponseData(value);
if (data) throw fromErrorResponseData(data, { statusCode: 502 });Parsing requires a nonblank string message, plus nonblank scope and code when present. A wrong type in any known field rejects the data. Blank optional details are omitted; other text stays unchanged. Unknown fields are ignored so new fields can be added later.
fromErrorResponseData() copies parsed fields into a VercelError. Parsing does not verify who sent the data. Check the source before forwarding details, and check permissions before acting on a fix or link.
All utilities below are exported from @vercel/error:
| Function | Behavior |
|---|---|
isVercelError(value) |
Recognize and narrow local instances or tagged data while the tag remains visible |
isError(value) |
Recognize standard errors across realms (iframes, workers, VM contexts) |
isErrorLike(value) |
Recognize an object with a string message |
hasCode(error, codeOrCodes) |
Narrow an error by one code or a readonly list |
getMessage(error, fallback?) |
Extract a message from an unknown value |
getRootCause(error) |
Follow cause to the root while stopping object cycles |
isError recognizes standard errors across realms. It uses Error.isError when available and falls back on older runtimes. errorResponse() applies separate disclosure checks, so fallback differences cannot expose developer details.
isVercelError recognizes local instances and tagged data while its symbol-keyed tag remains observable. The tag does not survive JSON, structured clone, Worker, or MessagePort transfer. Use ErrorResponseData for client-safe serialized data and an application-owned protocol for full diagnostics.
Tagged matches do not prove the producer is trusted. Their metadata and attributes remain unknown. Use instanceof VercelError before calling class methods or relying on typed local diagnostics.
Size Limit measures the minified, Brotli-compressed bundle produced when only one runtime export is imported from the built package, including the code it depends on. CI enforces a separate budget for every runtime export.
Regenerate with
pnpm size:readme.
| Entry point or export | Size (min+brotli) |
|---|---|
| @vercel/error | |
VercelError |
1.73 kB |
createErrors |
1.95 kB |
isVercelError |
1.88 kB |
isError |
108 B |
isErrorLike |
85 B |
hasCode |
120 B |
getMessage |
187 B |
getRootCause |
139 B |
| @vercel/error/client | |
fromErrorResponseData |
1.8 kB |
fromHttpResponse |
2.09 kB |
parseErrorResponseData |
373 B |
| @vercel/error/server | |
buildErrorResponseData |
2.25 kB |
errorResponse |
2.94 kB |
wantsAnsi |
505 B |
| @vercel/error/format | |
formatError |
1.2 kB |
frame |
1.06 kB |
hint |
52 B |
fix |
51 B |
link |
51 B |