import*aserrorsfrom'@superbuilders/errors';// 🚫 Instead of this:asyncfunctionoldFetchAndProcess(userId: string){try{constresponse=awaitfetch(`/api/users/${userId}`);if(!response.ok){thrownewError(`API request failed with status ${response.status}`);}constuser=awaitresponse.json();constprocessedData=awaitprocessData(user);returnprocessedData;}catch(error: any){// Context can be lost or manually (and verbosely) reconstructedconsole.error(`Operation failed for user ${userId}: ${error.message}`);// Re-throwing often loses the original error type and stackthrownewError(`User data processing chain failed: ${error.message}`);}}// ✅ Do this with @superbuilders/errors:asyncfunctionnewFetchAndProcess(userId: string){constresponseResult=awaiterrors.try(fetch(`/api/users/${userId}`));if(responseResult.error){// Preserves original error, adds specific contextthrowerrors.wrap(responseResult.error,`API request for user ${responseResult.error.value}`);}if(!responseResult.data.ok){// Create a new, specific errorthrowerrors.new(`API request failed with status ${responseResult.data.status}`);}constuserResult=awaiterrors.try(responseResult.data.json());if(userResult.error){throwerrors.wrap(userResult.error,`parsing user JSON for ${userId}`);}constprocessedResult=awaiterrors.try(processData(userResult.data));if(processedResult.error){throwerrors.wrap(processedResult.error,`processing data for user ${userId}`);}returnprocessedResult.data;// Safely access data}
Key Benefits:
🎯 Type-Safe Results: result.data and result.error are properly typed and discriminated.
🔗 Rich Error Context: Errors accumulate context (e.g., "processing data for user admin: parsing user JSON for admin: API returned invalid JSON").
Go Go-Inspired Simplicity: Handle errors with a clear if (result.error) check, similar to Go's if err != nil.
🧹 Cleaner Code: Decouples the happy path from error handling logic.
🚫 Eliminate try/catch: Adopt a more robust and consistent error handling pattern across your codebase.
npm install @superbuilders/errors
# or
yarn add @superbuilders/errors
# or
pnpm add @superbuilders/errors
# or
bun add @superbuilders/errors
Core Philosophy: Never Use try/catch Again
This library is designed as a complete replacement for try/catch blocks. Once you adopt @superbuilders/errors, you should aim to eliminate try/catch from your application logic.
The fundamental pattern is:
Perform an operation using errors.try() (for async) or errors.trySync() (for sync).
Immediately check the error property of the result.
If an error exists, handle it (often by throw errors.wrap(result.error, "context") or throw errors.new("new error")).
If no error, proceed with result.data.
import*aserrorsfrom'@superbuilders/errors';// Example of the core patternasyncfunctionfetchImportantData(id: string){constresult=awaiterrors.try(someAsyncOperation(id));// CRITICAL: Check for error immediatelyif(result.error){// Add context and propagatethrowerrors.wrap(result.error,`fetching important data for id ${id}`);}// If we're here, result.data is available and typedconsole.log("Success:",result.data);returnresult.data;}
errors.try<T, E extends Error = Error>(promise: Promise<T>): Promise<Result<T, E>>
Replaces async try/catch blocks. Wraps a Promise and returns a Result object.
// ❌ Before:asyncfunctionfetchDataOld(url: string){try{constresponse=awaitfetch(url);returnawaitresponse.json();}catch(error){console.error("Fetch failed:",error);throwerror;// Or wrap manually: new Error(`Failed: ${error.message}`)}}// ✅ After:asyncfunctionfetchDataNew(url: string){constresponseResult=awaiterrors.try(fetch(url));if(responseResult.error){throwerrors.wrap(responseResult.error,`network request to ${url}`);}constjsonResult=awaiterrors.try(responseResult.data.json());if(jsonResult.error){throwerrors.wrap(jsonResult.error,`parsing JSON from ${url}`);}returnjsonResult.data;}
Important: Always check result.error immediately after the errors.try call.
errors.wrap<E extends Error>(originalError: E, message: string): Readonly<WrappedError<E>>
Adds context to an existing error while preserving the original error and its stack trace. This is key to building informative error chains.
Use errors.wrap primarily for errors originating from errors.try, errors.trySync, or external libraries.
Do NOT wrap errors you created with errors.new(). Just throw the errors.new() error directly or create a new one with more context.
// dbCall() might throw an error from a database driverconstresult=awaiterrors.try(dbCall());if(result.error){// ✅ CORRECT: Wrapping an external/caught errorthrowerrors.wrap(result.error,"database operation failed");}// ❌ AVOID: Wrapping an error you just created// const myError = errors.new("something specific went wrong");// throw errors.wrap(myError, "operation failed"); // Redundant, just make the first message better// ✅ BETTER for self-created errors:if(condition){throwerrors.new("operation failed: something specific went wrong");}
Message Style: Use lowercase, terse, context-focused descriptions (Go style).
Finds the root cause in an error chain. Traverses the .cause properties.
constdbErr=errors.new("connection timeout");constserviceErr=errors.wrap(dbErr,"user service query");constapiErr=errors.wrap(serviceErr,"GET /api/users");constrootCause=errors.cause(apiErr);console.log(rootCause.message);// "connection timeout"// Type of rootCause can be inferred if the chain is typed
errors.is<T extends Error, U extends Error>(error: T, target: U): boolean
Checks if a specific error instance exists anywhere in the error chain. Compares by reference (===).
constErrTimeout=errors.new("request timed out");// Create a sentinel errorasyncfunctionoperationWithRetry(){constresult=awaiterrors.try(apiCall());if(result.error){if(errors.is(result.error,ErrTimeout)){// Specific retry logic for timeoutsconsole.log("Operation timed out, retrying...");// ... retry logic ...}throwerrors.wrap(result.error,"apiCall");}returnresult.data;}
errors.as<T extends Error, U extends Error>(error: T, ErrorClass: new (...args: any[]) => U): U | undefined
Checks if an error in the chain is an instance of a specific error class and returns it, allowing type-safe access to custom error properties.
classNetworkErrorextendsError{constructor(message: string,publicstatusCode: number){super(message);this.name="NetworkError";}}functionhandleApiError(err: Error){constnetworkErr=errors.as(err,NetworkError);if(networkErr){console.log(`Network error with status: ${networkErr.statusCode}`);if(networkErr.statusCode===503){// schedule retry}return;}// Handle other errors or re-throwthrowerr;}// Usage:constapiResult=awaiterrors.try(fetchFromApi());if(apiResult.error){handleApiError(apiResult.error);}
The Power of Chained Context
With proper use of errors.wrap, your error messages become incredibly informative:
Imagine an error occurs deep within a series of calls:
fs.readFile fails with ENOENT: no such file or directory.
Without @superbuilders/errors, you might just see:
Error: ENOENT: no such file or directory
(Where? Why was it being read?)
With @superbuilders/errors:
Error: processing user config: reading user settings file: /home/user/.myapp/settings.json: ENOENT: no such file or directory
This tells you the full story:
The overall operation was "processing user config".
Which involved "reading user settings file".
Specifically the file "/home/user/.myapp/settings.json".
And the root cause was ENOENT: no such file or directory.
This drastically reduces debugging time.
@superbuilders/errors is written in TypeScript and provides strong type safety:
Discriminated Unions for Result:
constresult=awaiterrors.try(fetchUserData());if(result.error){// result.data is undefined here, TypeScript knows!// result.error is typed (Error by default, or specify E in errors.try<T,E>)handleError(result.error);}else{// result.error is undefined here, TypeScript knows!// result.data is typed (T)processUserData(result.data);}
WrappedError<C> and DeepestCause<E> Types: Exported types WrappedError and DeepestCause allow you to precisely type your error chains and their root causes if needed.
Type Inference: TypeScript often infers the types correctly, reducing boilerplate.
API Operations with Fallbacks
asyncfunctiongetUserPreferred(id: string){constprimaryResult=awaiterrors.try(primaryApi.getUser(id));if(!primaryResult.error)returnprimaryResult.data;console.warn(`Primary API failed for user ${id}: ${primaryResult.error.toString()}`);constbackupResult=awaiterrors.try(backupApi.getUser(id));if(!backupResult.error)returnbackupResult.data;console.warn(`Backup API failed for user ${id}: ${backupResult.error.toString()}`);throwerrors.wrap(backupResult.error,`all user sources failed for ${id}`);}
asyncfunctionupdateUserBalance(userId: string,amount: number){consttx=awaitdb.beginTransaction();// Assume this can't fail or has its own error systemconstcurrentBalanceResult=awaiterrors.try(tx.query("SELECT balance FROM users WHERE id = ?",[userId]));if(currentBalanceResult.error){awaiterrors.try(tx.rollback());// Log rollback error if it occursthrowerrors.wrap(currentBalanceResult.error,`fetching balance for user ${userId}`);}constnewBalance=currentBalanceResult.data[0].balance+amount;constupdateResult=awaiterrors.try(tx.query("UPDATE users SET balance = ? WHERE id = ?",[newBalance,userId]));if(updateResult.error){awaiterrors.try(tx.rollback());throwerrors.wrap(updateResult.error,`updating balance for user ${userId}`);}constcommitResult=awaiterrors.try(tx.commit());if(commitResult.error){// Data might be in an inconsistent state or commit failed after successful opsthrowerrors.wrap(commitResult.error,`committing transaction for user ${userId}`);}return{ newBalance };}
Immediate Error Checking: Always check result.error on the line(s) immediately following an errors.try or errors.trySync call. Don't intersperse other logic.
// ✅ CORRECTconstresult=awaiterrors.try(operation());if(result.error){/* handle or throw */}// ❌ AVOIDconstresult=awaiterrors.try(operation());// ... other logic ...if(result.error){/* handle or throw */}
Propagate or Handle Deliberately: If an error occurs, either wrap it and re-throw it to a higher-level handler, or handle it specifically at the current level. Don't just console.error and continue as if nothing happened (unless that's truly the desired behavior for minor, recoverable issues).
Use errors.new for Your Errors: When you detect an error condition in your own logic (e.g., invalid input, failed business rule), create errors with errors.new("descriptive message").
Use errors.wrap for External/Caught Errors: When an error comes from an external library, a native function, or is caught by errors.try/errors.trySync, use errors.wrap(err, "context") to add your application's context.
Terse, Lowercase Context Messages: When wrapping, keep context messages concise, lowercase, and focused on what your code was trying to do. E.g., "authenticating user", "reading config file".
Leverage errors.as for Custom Error Types: If you have custom error classes with specific properties, use errors.as(err, MyCustomError) to safely access those properties.
Refactoring an existing codebase to use @superbuilders/errors involves two main steps:
if(value<0){thrownewError("Value cannot be negative.");}
After:
if(value<0){throwerrors.new("value cannot be negative");}
This library is heavily inspired by the robust error handling patterns from the Go programming language and the excellent efficientgo/core library for Go. The goal is to bring similar clarity, context preservation, and predictability to the TypeScript/JavaScript ecosystem. While this is an independent implementation, we acknowledge and appreciate the foundational ideas demonstrated by these Go patterns.
Contributions are welcome! If you have ideas for improvements or find any issues, please open an issue or submit a pull request. The core philosophy is to provide a complete, elegant, and type-safe replacement for try/catch, so changes should align with this goal.
0BSD. This library is free to use, modify, and distribute.