⚠️ Warning: This project must be considered educational and is not meant for production use; see Rationale below.
An opinionated TypeScript wrapper/extension for neverthrow that enriches Result and ResultAsync with structured metadata, error codes, and observability features while preserving the original ergonomics.
Although neverthrow can embed complex metadata directly in its Error type, this project implements a wrapper pattern with compatible exports, serving as a drop-in companion during refactors to add observability metadata without altering existing error types—ideal for neverthrow codebases needing extra debugging insight.
- Rich Metadata: Attach timestamps, error codes, severity levels, and custom metadata to every result
- Error Translation: Convert thrown exceptions into structured, typed errors with
wrapSync/wrapAsync - Standard Error Codes: Built-in error codes for common scenarios (network, validation, parsing, etc.)
- Type Safety: Full TypeScript support with generic types
- Observability Ready: Structured data perfect for logging, monitoring, and debugging
- Zero Dependencies: Only requires neverthrow as a peer dependency
import { mok, merr, mwrapSync } from 'neverthrow-metadata';
// Success with metadata
const user = mok({ id: 'usr_2Nq8x9P4mL7vK3Fn', name: 'Sarah Chen' }, {
source: 'database'
});
// Error with structured metadata
const error = merr(new Error('User not found'), 'USER_NOT_FOUND', {
severity: 'medium',
retryable: false
});
// Wrap risky operations
const parsed = mwrapSync(() => JSON.parse(data), 'PARSE_ERROR');import {
MetadataWrapper,
MetadataUtils,
StandardErrorCodes,
Severity,
mok,
merr,
mwrap,
mwrapSync
} from 'neverthrow-metadata';
const success = mok('cached-user-profile', { source: 'cache' });
const failure = merr(new Error('not found'), StandardErrorCodes.RESOURCE_NOT_FOUND, {
severity: Severity.Medium,
userMessage: 'User could not be located'
});
if (success.isOk()) {
console.log(success.value.value); // => 'cached-user-profile'
}
if (failure.isErr()) {
console.log(failure.error.metadata.code); // => 'RESOURCE_NOT_FOUND'
}
const parsed = mwrapSync(() => JSON.parse('{"ok":true}'), StandardErrorCodes.PARSE_ERROR);
const fetched = await mwrap(async () => fetchUser(), StandardErrorCodes.NETWORK_ERROR);For production applications, you can enrich metadata with request tracing context:
import { mok, merr, mwrap } from 'neverthrow-metadata';
const success = mok('cached-user-profile', {
source: 'redis-cluster',
duration: 12,
details: {
'x-request-id': 'req-123',
'x-trace-id': 'trace-456',
orgId: 'example-enterprise',
userId: 'user-789',
cacheKey: 'user:profile:user-789'
}
});
const failure = merr(new Error('Database connection timeout'), StandardErrorCodes.RESOURCE_NOT_FOUND, {
severity: Severity.Medium,
userMessage: 'Unable to load user profile at this time',
details: {
'x-request-id': 'req-123',
'x-trace-id': 'trace-456',
orgId: 'example-enterprise',
userId: 'user-789',
dbHost: 'pg-prod-read-02.example-db.com'
}
});
if (success.isOk()) {
console.log(success.value.value); // => 'cached-user-profile'
console.log(success.value.metadata.details?.orgId); // => 'example-enterprise'
}
if (failure.isErr()) {
console.log(failure.error.metadata.code); // => 'RESOURCE_NOT_FOUND'
console.log(failure.error.metadata.details?.['x-request-id']); // => 'req-123'
}
const fetched = await mwrap(async () => fetchUserProfile('user-789'), StandardErrorCodes.NETWORK_ERROR, {
details: {
'x-request-id': 'req-123',
'x-trace-id': 'trace-456',
operationName: 'user.fetchProfile',
userId: 'user-789'
}
});const retryable = MetadataWrapper.failure(new Error('timeout'), StandardErrorCodes.NETWORK_ERROR, {
retryable: true,
severity: Severity.High
});
console.log(MetadataUtils.isRetryable(retryable)); // true
console.log(MetadataUtils.getSeverity(retryable)); // 'high'ControlFlow.retry and retryAsync can be used when you need simple retry loops based on the metadata returned from your operations.
When control-flow helpers translate failures into metadata results they emit dedicated codes exposed via InternalMetadataErrorCodes. Use these to distinguish between synchronous throws (CONTROL_FLOW_SYNC_THROW), asynchronous rejections (CONTROL_FLOW_ASYNC_REJECTION), and operations that timed out (CONTROL_FLOW_TIMEOUT_EXCEEDED).
gRPC error status support is also baked in via GrpcErrorCodes and the matching GrpcStatusMetadata.
const status = GrpcStatusMetadata[GrpcErrorCodes.UNAVAILABLE];
const unavailable = MetadataWrapper.failure(
new Error('service down'),
'GRPC_UNAVAILABLE',
{
severity: status.defaultSeverity,
retryable: status.defaultRetryable,
details: { category: status.category }
}
);You can also define your own error codes:
const CUSTOM_CODE = 'MY_DOMAIN_ERROR';
const enriched = MetadataWrapper.failure(new Error('domain failure'), CUSTOM_CODE, {
severity: Severity.High,
retryable: true,
details: { feature: 'billing' }
});All helpers are fully typed:
import type { MetadataResult } from 'neverthrow-metadata';
interface User {
id: string;
email: string;
}
function loadUser(id: string): MetadataResult<User, Error> {
return mwrapSync(() => {
const user = maybeFindUser(id);
if (!user) {
throw new Error('User missing');
}
return user;
}, StandardErrorCodes.RESOURCE_NOT_FOUND);
}The library supports rich request tracing metadata through the details field, making it perfect for distributed systems observability:
import { mok, merr, mwrap } from 'neverthrow-metadata';
// HTTP request with full trace context
const ctx = {
'x-request-id': 'req-123',
'x-trace-id': 'trace-456',
'x-span-id': 'span-auth',
'x-correlation-id': 'login-flow-789',
orgId: 'example-saas-platform',
userId: 'user-abc',
sessionId: 'session-xyz',
operationName: 'user.authenticate'
};
// Database operation with request correlation
const userQuery = await mwrap(async () => {
return await db.users.findById('user-abc');
}, 'DATABASE_ERROR', {
details: {
...ctx,
query: 'SELECT * FROM users WHERE id = $1',
dbInstance: 'pg-prod-read-02.example-db.com'
}
});
// Microservice call with trace propagation
const paymentResult = await mwrap(async () => {
return await paymentService.createPayment({
amount: 2999,
currency: 'usd'
});
}, 'PAYMENT_ERROR', {
details: {
...ctx,
'x-span-id': 'span-payment', // New child span
'x-parent-span-id': 'span-auth',
serviceUrl: 'https://api.example-payments.com/v1/payments',
amount: 2999,
customerId: 'customer-123'
}
});Rich error context makes debugging production issues much easier:
const validationError = merr(
new Error('Personal email domains are not permitted for business accounts'),
'VALIDATION_ERROR',
{
severity: Severity.Medium,
retryable: false,
userMessage: 'Please use your business email address',
details: {
'x-request-id': 'req-456',
'x-trace-id': 'trace-789',
orgId: 'example-enterprise',
userId: 'user-def',
inputEmail: 'john.doe@personal-email.com',
emailDomain: 'personal-email.com',
validationRule: 'enterprise_email_domains_only',
clientIp: '203.0.113.42',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
}
);import { mwrap, StandardErrorCodes } from 'neverthrow-metadata';
const fetchUser = async (id: string, ctx: any) => {
const result = await mwrap(
() => fetch(`/api/users/${id}`).then(r => r.json()),
StandardErrorCodes.NETWORK_ERROR,
{
retryable: true,
details: {
...ctx,
operationName: 'api.users.fetch',
timeoutMs: 5000,
endpoint: `/api/users/${id}`,
method: 'GET'
}
}
);
return result;
};import { ControlFlow } from 'neverthrow-metadata';
const result = await ControlFlow.retryAsync(
() => fetchUser('123'),
3, // max attempts
200 // base delay in milliseconds
);MetadataWrapper.wrapResult lets you take an existing Result from neverthrow and convert it into a metadata-aware result, and MetadataUtils.unwrapResult converts back to plain neverthrow results when you need to interoperate with existing code. Aliases enhanceResult and stripResult remain for backward compatibility.
This project is licensed under the MIT License - see the LICENSE file for details.
Run the provided examples to see the library in action:
# Run all examples
make examples
# Run individual examples
npm run example:basic # Basic patterns and request tracing
npm run example:advanced # Advanced risky operations with rich metadataContributions are welcome, please follow the semantic versioning branch naming convention:
- main: Production-ready code
- develop: Integration branch for features
- feat/: New features (
feat/user-authentication) - fix/: Bug fixes (
fix/connection-timeout) - chore/: Maintenance (
chore/update-dependencies)
Michele Tavella - meeghele@proton.me