
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
ts-error-tuple
Advanced tools
A concise and type-safe error handling library for TypeScript that mimics Golang's simple and explicit error handling.
A concise and type-safe error handling library for TypeScript that mimics Golang's simple and explicit error handling, with added support for error context.
This allows you to treat errors as values so you can write more safe, readable, and maintainable code.
bun add ts-error-tuple
# npm install ts-error-tuple
This pattern should look familiar if you've used Golang:
import type { Result } from "ts-error-tuple"
import { attempt, err } from "ts-error-tuple"
function getUserById(id: number): Result<User> {
// Use the attempt function to handle potential thrown errors from external code
const [user, error] = attempt(() => db.findUser(id)) // pretend that db.findUser throws an Error with the message: "failed to connect to database"
if (error) return [undefined, err("failed to find user", error)]
return [user, undefined]
}
const [user, error] = getUserById(123)
if (error) console.error(error.fmtErr())
// "failed to find user -> failed to connect to database"
else console.log(`Hello, ${user.name}!`)
You can also add context to errors to help with debugging and logging:
function getUserById(id: number): Result<User> {
const [user, error] = attempt(() => db.findUser(id))
if (error) {
// Add context to the error before returning it
return [
undefined,
err("failed to find user", error).ctx({
userId: id,
operation: "findUser",
timestamp: new Date().toISOString(),
}),
]
}
return [user, undefined]
}
const [user, error] = getUserById(123)
if (error) {
console.error(error.fmtErr()) // "failed to find user -> failed to connect to database"
// Get the context for logging or debugging
console.log(`Error for user ID: ${error.get("userId")}`)
console.log(`Operation: ${error.get("operation")}`)
console.log(`Timestamp: ${error.get("timestamp")}`)
} else console.log(`Hello, ${user.name}!`)
Just like is common in Golang, errors are propagated up the call stack which helps build more useful error messages.
If applied correctly and consistently, all errors throughout your codebase are checked and handled immediately.
Please see the API description for more details and examples. Also see API for a more in-depth example.
Many modern programming languages treat errors as values, and for good reason. It leads to more reliable and maintainable code by forcing error handling to be explicit. In other words, it almost completely eliminates runtime crashes due to unhandled exceptions, which is a very common problem in JS/TS.
As an alternative, there are many other libraries available that are inspired by Rust's Result type. Just to name a few:
However, these libraries tend to be considerably more complex and have a much larger API surface. In contrast, ts-error-tuple is only ~60 lines of code.
I find Golang's approach to error handling more clear and easy to reason about, which is what this library aims to provide.
In an effort to keep the API concise, ts-error-tuple only exports four things:
Result Typeattempt Functionerr Function
CtxError Class is not exported and is instead accessed via errErr TypeResult Typetype Result<T> = [T, undefined] | [undefined, CtxError]
Result represents a tuple that contains a value or an error.
undefinedundefinedThe main idea is that when you would normally write a function that returns T, you should instead return Result<T>.
undefined) should use the Err type instead.Instead of this:
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("division by zero")
}
return a / b
}
try {
const result = divide(10, 0)
console.log(result)
} catch (err) {
console.error("failed to divide", err)
}
Use Result:
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return [undefined, err("division by zero")]
}
return [a / b, undefined]
}
const [result, error] = divide(10, 0)
if (error) {
console.error(error.fmtErr("failed to divide")) // "failed to divide -> division by zero"
} else {
console.log(result)
}
attempt Functionattempt executes a function, catches any errors thrown, and returns a Result.
It is generally used for functions that you don't control which might throw an error.
attempt to "force" functions to return a Result so error handling remains consistentattempt should be used as far down the call stack as possible so that thrown errors are handled at their source// in some function that returns a Result
const [file, error] = attempt(() => fs.readFileSync("non-existent.json"))
if (error) {
return [undefined, err("failed to read file", error)]
}
// do something with `file`
The function can be either synchronous or asynchronous.
Result will be a Promise and should be awaited// fs.readFile returns a Promise
const [file, error] = await attempt(() => fs.readFile("file.txt"))
In the case where a function doesn't return anything (i.e. undefined) but might fail, you can simply ignore the first element of the Result.
const [, error] = attempt(() => fs.rmSync("non-existent.txt"))
if (error) {
console.error(error.fmtErr("failed to remove file")) // "failed to remove file -> ENOENT: no such file or directory"
}
err Functionerr(message: string, cause?: unknown): CtxError
err takes a message and a cause (optional) and returns a new CtxError.
new CtxError() to make creating errors more conciseconst error = err("something went wrong", originalError)
Equivalent to:
const error = new CtxError("something went wrong", { cause: originalError })
CtxError ClassCtxError is a custom error class that extends the built-in Error class with additional functionality.
CtxError are instances of Error, so using them in place of Error won't cause any issues.ctx Methodctx(context: Record<string, unknown>): CtxError
Adds context to the error.
const error = err("failed to process request").ctx({ requestId: "abc-123" })
console.log(error.context) // { requestId: "abc-123" }
If the error already has context, the new context will be merged over the existing context.
const error = err("failed to process request").ctx({ requestId: "abc-123", userId: 123 })
// ... later on
error.ctx({ requestId: "cba-321" })
console.log(error.context) // { requestId: "cba-321", userId: 123 }
get Methodget<T>(key: string): T | undefined
Retrieves a context value from the error chain (this error and all its causes), prioritizing the deepest value.
// imagine these errors are propagated up through various function calls
const deepError = err("failed to connect to database").ctx({ logScope: "database" })
const middleError = err("failed to do the thing", deepError).ctx({ logScope: "service" })
const topError = err("failed to process request", middleError).ctx({ logScope: "controller" })
console.log(topError.get("scope")) // "database" (from deepError)
fmtErr MethodfmtErr(message?: string): string
Returns a nicely formatted error message by unwrapping all error messages in the error chain (this error and all its causes).
message -> cause1 -> cause2 -> ... -> causeN// imagine these errors are propagated up through various function calls
const deepError = err("failed to connect to database")
const middleError = err("failed to do the thing", deepError)
const topError = err("failed to process request", middleError)
console.log(topError.fmtErr())
// "failed to process request -> failed to do the thing -> failed to connect to database"
// With the optional message given
console.log(topError.fmtErr("something went wrong"))
// "something went wrong -> failed to process request -> failed to do the thing -> failed to connect to database"
Err Typetype Err = CtxError | undefined
Err is a type alias for CtxError | undefined to make function return types more readable.
function validateData(data: string): Err {
if (data.length < 10) {
return err("data is too short")
}
// implicit return undefined
}
const error = validateData("short")
if (error) {
console.error(error.fmtErr("failed to validate data")) // "failed to validate data -> data is too short"
}
This example is a bit contrived, but use your imagination :)
Putting it all together:
import type { Err, Result } from "ts-error-tuple"
import { attempt, err } from "ts-error-tuple"
// Pretend this "db" module doesn't belong to us and its functions might throw errors
const db = {
connect: (dbId: string) => {
throw new Error("invalid dbId")
},
query: (queryString: string) => {
throw new Error("invalid query")
},
}
// This function doesn't return a value (other than a possible error), so we use `Err`
function connectToDb(dbId: string): Err {
// Use attempt because db.connect() might throw
const [, error] = attempt(() => db.connect(dbId))
if (error) {
return err("failed to connect to database", error).ctx({
timestamp: new Date().toISOString(),
logScope: "db-connect",
})
}
return undefined
}
// A function that returns a `Result`
async function queryDb(queryString: string): Promise<Result<DbQuery>> {
const dbConnectionError = connectToDb("db-prod-1")
if (dbConnectionError) {
// We don't need to provide an additional message or context here, so we return the error directly instead of using `err`
return [undefined, dbConnectionError]
}
const [data, queryError] = await attempt(() => db.query(queryString))
if (queryError) {
return [undefined, err("failed to query db", queryError).ctx({ queryString, logScope: "db-query" })]
}
return [data, undefined]
}
async function main(): Promise<Result<Meeting[]>> {
const [meetings, meetingsQueryError] = await queryDb("SELECT * FROM meetings WHERE scheduled_time < actual_end_time")
if (meetingsQueryError) {
return [undefined, err("failed to get meetings", meetingsQueryError).ctx({ logScope: "main" })]
}
return [meetings, undefined]
}
const [result, error] = await main()
if (error) {
logger.error(error.fmtErr("something went wrong"), error) // we pass the error so the logger can call .get() to get the context
} else console.log(result)
In the above, let's say we have some logger that is able to handle our context. We'll pretend that it prefixes our error message with the context as a nicely formatted string. e.g. ${timestamp}[${logScope}]
Looking at the example, we have two situations where there could be an error: 1. db.connect() 2. db.query()
Let's see what each error would look like:
For db.connect():
2025-02-28T16:51:01.378Z [db-connect] something went wrong -> failed to get meetings -> failed to connect to database -> invalid dbId
For db.query():
[db-query] something went wrong -> failed to get meetings -> failed to query db -> invalid query: for 'SELECT \* FROM meetings WHERE scheduled_time < actual_end_time'
In this case, our logger appended : for '${queryString}'
Note that in both cases, the "deeper" logScope won.
FAQs
A concise and type-safe error handling library for TypeScript that mimics Golang's simple and explicit error handling.
We found that ts-error-tuple demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 0 open source maintainers 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
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.