
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
wellcrafted
Advanced tools
Define your errors. Type the rest.
Tagged errors and Result types as plain objects. < 2KB, zero dependencies.
Most Result libraries hand you a container and leave the error type as an exercise. You get Ok and Err but nothing to help you define, compose, or serialize the errors themselves. So you end up with string literals, ad-hoc objects, or class hierarchies that break the moment you call JSON.stringify.
wellcrafted takes the opposite approach: start with the errors. defineErrors gives you typed, serializable, composable error variants inspired by Rust's thiserror. The Result type is just { data, error } destructuring — the same shape you already know from Supabase, SvelteKit load functions, and TanStack Query. No .isOk() method chains, no .map().andThen().orElse() pipelines. Check error, use data. That's it.
import { defineErrors, extractErrorMessage, type InferErrors } from "wellcrafted/error";
import { tryAsync, Ok, type Result } from "wellcrafted/result";
// Define domain errors — all variants in one call
const UserError = defineErrors({
AlreadyExists: ({ email }: { email: string }) => ({
message: `User ${email} already exists`,
email,
}),
CreateFailed: ({ email, cause }: { email: string; cause: unknown }) => ({
message: `Failed to create user ${email}: ${extractErrorMessage(cause)}`,
email,
cause,
}),
});
type UserError = InferErrors<typeof UserError>;
// ^? { name: "AlreadyExists"; message: string; email: string }
// | { name: "CreateFailed"; message: string; email: string; cause: unknown }
// Each factory returns Err<...> directly — no wrapping needed
async function createUser(email: string): Promise<Result<User, UserError>> {
const existing = await db.findByEmail(email);
if (existing) return UserError.AlreadyExists({ email });
return tryAsync({
try: () => db.users.create({ email }),
catch: (error) => UserError.CreateFailed({ email, cause: error }),
});
}
// Discriminate with switch — TypeScript narrows automatically
const { data, error } = await createUser("alice@example.com");
if (error) {
switch (error.name) {
case "AlreadyExists": console.log(error.email); break;
case "CreateFailed": console.log(error.email); break;
// ^ TypeScript knows exactly which fields exist
}
}
npm install wellcrafted
You can use Ok and Err with any value. So why bother with defineErrors?
Because in practice, errors aren't random. Every service has a handful of things that can go wrong, and you want to enumerate them upfront. A user service has AlreadyExists, CreateFailed, InvalidEmail. An HTTP client has Connection, Timeout, Response. These are logical groups — the error vocabulary for a domain. Rust codified this with thiserror. defineErrors brings the same pattern to TypeScript, but outputs plain objects instead of classes.
Errors are data, not classes. Plain frozen objects with no prototype chain. JSON.stringify just works — no stack property eating up your logs, no instanceof checks that break across package boundaries. This matters anywhere errors cross a serialization boundary: Web Workers, server actions, sync engines, IPC. The error you create is the error that arrives.
Every factory returns Err<...> directly. No wrapping step. Return it from a tryAsync catch handler or as a standalone early return — if (existing) return UserError.AlreadyExists({ email }). The Result type flows naturally.
Discriminated unions for free. switch (error.name) gives you full TypeScript narrowing. No instanceof, no type predicates, no manual union types. Add a new variant and every consumer that switches gets a compile error until they handle it.
trySync and tryAsync turn throwing operations into Result types. The catch handler receives the raw error and returns an Err<...> from your defineErrors factories.
import { trySync, tryAsync } from "wellcrafted/result";
const JsonError = defineErrors({
ParseFailed: ({ input, cause }: { input: string; cause: unknown }) => ({
message: `Invalid JSON: ${extractErrorMessage(cause)}`,
input: input.slice(0, 100),
cause,
}),
});
// Synchronous
const { data, error } = trySync({
try: () => JSON.parse(rawInput),
catch: (cause) => JsonError.ParseFailed({ input: rawInput, cause }),
});
// Asynchronous
const { data, error } = await tryAsync({
try: () => fetch(url).then((r) => r.json()),
catch: (cause) => HttpError.Connection({ url, cause }),
});
When catch returns Ok(fallback) instead of Err, the return type narrows to Ok<T> — no error checking needed:
const { data: parsed } = trySync({
try: (): unknown => JSON.parse(riskyJson),
catch: () => Ok([]),
});
// parsed is always defined — the catch recovered
This is where the pattern pays off. Each layer defines its own error vocabulary; inner errors become cause fields, and extractErrorMessage formats them inside the factory so call sites stay clean.
// Service layer: domain errors wrap raw failures via cause
const UserServiceError = defineErrors({
NotFound: ({ userId }: { userId: string }) => ({
message: `User ${userId} not found`,
userId,
}),
FetchFailed: ({ userId, cause }: { userId: string; cause: unknown }) => ({
message: `Failed to fetch user ${userId}: ${extractErrorMessage(cause)}`,
userId,
cause,
}),
});
type UserServiceError = InferErrors<typeof UserServiceError>;
async function getUser(userId: string): Promise<Result<User, UserServiceError>> {
const response = await tryAsync({
try: () => fetch(`/api/users/${userId}`),
catch: (cause) => UserServiceError.FetchFailed({ userId, cause }),
// raw fetch error becomes cause ^^^^^
});
if (response.error) return response;
if (response.data.status === 404) return UserServiceError.NotFound({ userId });
return tryAsync({
try: () => response.data.json() as Promise<User>,
catch: (cause) => UserServiceError.FetchFailed({ userId, cause }),
});
}
// API handler: maps domain errors to HTTP responses
async function handleGetUser(request: Request, userId: string) {
const { data, error } = await getUser(userId);
if (error) {
switch (error.name) {
case "NotFound":
return Response.json({ error: error.message }, { status: 404 });
case "FetchFailed":
return Response.json({ error: error.message }, { status: 502 });
}
}
return Response.json(data);
}
The full error chain is JSON-serializable at every level. Log it, send it over the wire, display it in a toast. The structure survives.
The foundation is a simple discriminated union:
import { Ok, Err, trySync, tryAsync, type Result } from "wellcrafted/result";
type Ok<T> = { data: T; error: null };
type Err<E> = { error: E; data: null };
type Result<T, E> = Ok<T> | Err<E>;
Check error first, and TypeScript narrows data automatically:
const { data, error } = await someOperation();
if (error) {
// error is E, data is null
return;
}
// data is T, error is null
Create distinct types from primitives so TypeScript catches mix-ups at compile time. Zero runtime footprint — purely a type utility.
import type { Brand } from "wellcrafted/brand";
type UserId = string & Brand<"UserId">;
type OrderId = string & Brand<"OrderId">;
function getUser(id: UserId) { /* ... */ }
const userId = "abc" as UserId;
const orderId = "xyz" as OrderId;
getUser(userId); // compiles
getUser(orderId); // type error
TanStack Query factories with a dual interface: .options for reactive components, callable for imperative use in event handlers.
import { createQueryFactories } from "wellcrafted/query";
const { defineQuery, defineMutation } = createQueryFactories(queryClient);
const userQuery = defineQuery({
queryKey: ["users", userId],
queryFn: () => getUser(userId), // returns Result<User, UserError>
});
// Reactive — pass to useQuery (React) or createQuery (Svelte)
const query = createQuery(() => userQuery.options);
// Imperative — direct execution for event handlers
const { data, error } = await userQuery.fetch();
| wellcrafted | neverthrow | better-result | fp-ts | Effect | |
|---|---|---|---|---|---|
| Error definition | defineErrors factories | Bring your own | TaggedError classes | Bring your own | Class-based with _tag |
| Error shape | Plain frozen objects | Any type | Class instances | Any type | Class instances |
| Composition | Manual if (error) | .map().andThen() | Result.gen() generators | Pipe operators | yield* generators |
| Bundle size | < 2KB | ~5KB | ~2KB | ~30KB | ~50KB |
| Syntax | async/await | Method chains | Method chains + generators | Pipe operators | Generators |
Every Result library gives you a container. wellcrafted gives you what goes inside it — then gets out of the way.
wellcrafted is deliberately idiomatic to JavaScript. The { data, error } shape isn't novel — it's the same pattern used by Supabase, SvelteKit load functions, and TanStack Query. We chose it because it's already familiar, already destructurable, and requires zero new mental models.
The same principle applies throughout: async/await instead of generators, switch instead of .match(), plain objects instead of class hierarchies. The best abstractions are the ones your team already knows. wellcrafted adds type-safe error definition on top of patterns that JavaScript developers use every day — it doesn't ask you to learn a new programming paradigm to handle errors.
defineErrors(config) — define multiple error factories in a single call. Each key becomes a variant; the value is a factory returning { message, ...fields }. Every factory returns Err<...> directly.extractErrorMessage(error) — extract a readable string from any unknown error value.InferErrors<T> — extract union of all error types from a defineErrors return value.InferError<T> — extract a single variant's error type from one factory.Ok(data) — create a success resultErr(error) — create a failure resulttrySync({ try, catch }) — wrap a synchronous throwing operationtryAsync({ try, catch }) — wrap an async throwing operationisOk(result) / isErr(result) — type guardsunwrap(result) — extract data or throw errorresolve(value) — handle values that may or may not be ResultspartitionResults(results) — split an array of Results into separate ok/err arrayscreateQueryFactories(client) — create query/mutation factories for TanStack QuerydefineQuery(options) — define a query with dual interface (.options for reactive, callable for imperative)defineMutation(options) — define a mutation with dual interfaceResultSchema(dataSchema, errorSchema) — Standard Schema wrapper for Result types, interoperable with any validator that supports the spec.Result<T, E> — union of Ok<T> | Err<E>Brand<T, B> — branded type wrapper for distinct primitivesIf you use an AI coding agent (Claude Code, Cursor, etc.), teach it how to use wellcrafted correctly:
npx skills add wellcrafted-dev/wellcrafted
This installs 5 skills that teach your agent the patterns, anti-patterns, and API conventions:
| Skill | What it teaches |
|---|---|
define-errors | defineErrors variants, extractErrorMessage, InferErrors/InferError type extraction |
result-types | Ok, Err, trySync/tryAsync, the { data, error } destructuring pattern |
query-factories | createQueryFactories, defineQuery/defineMutation, dual interface (reactive + imperative) |
branded-types | Brand<T>, brand constructor pattern, when to add runtime validation |
patterns | Architectural style guide: control flow, factory composition, service layers, error composition |
Skills work with any agent that supports npx skills. Install once, update with npx skills update.
AI agent skills are managed via npx skills, sourced from Epicenter. Only skills relevant to wellcrafted's domain are installed.
# Install skills (already committed, but can be refreshed)
npx skills add EpicenterHQ/epicenter --skill error-handling --skill define-errors -a claude-code -y
# Update all installed skills
npx skills update
# List installed skills
npx skills list
MIT
FAQs
Delightful TypeScript patterns for elegant, type-safe applications
The npm package wellcrafted receives a total of 671 weekly downloads. As such, wellcrafted popularity was classified as not popular.
We found that wellcrafted demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.