UNPKG

go-go-try

Version:

Tries to execute a sync/async function, returns a result tuple

640 lines (487 loc) 20 kB
# `go-go-try` > Tries to execute a sync/async function, returns a Golang style result. ## Why - Supports sync/async functions. - Allows you to capture the thrown error. - Written in TypeScript. The types are written in a way that reduce developer errors. - Inspired by Golang error catching. - Zero dependencies. Why not just `try`/`catch`? - In a lot of cases, `try`/`catch` is still the better option. - Nested `try`/`catch` statements are hard to process mentally. They also indent the code and make it hard to read. A single `try`/`catch` does the same but to a lesser degree. - If you [prefer const](https://eslint.org/docs/latest/rules/prefer-const), `try`/`catch` statements get in the way because you need to use `let` if you need the variable outside of the `try`/`catch` scope: ```ts let todos try { todos = JSON.parse(localStorage.getItem('todos')) } catch {} return todos.filter((todo) => todo.done) ``` - It takes more space. It's slower to type. ## Install ```bash npm install go-go-try ``` ## Basic Usage ```ts import { goTry, goTryRaw } from 'go-go-try' // tries to parse todos, returns empty array if it fails const [_, value = []] = goTry(() => JSON.parse(todos)) // fetch todos, on error, fallback to empty array const [_, todos = []] = await goTry(fetchTodos()) // fetch todos, fallback to empty array, send error to your error tracking service const [err, todos = []] = await goTry(fetchTodos()) // err is string | undefined if (err) sendToErrorTrackingService(err) // goTry extracts the error message from the error object, if you want the raw error object, use goTryRaw const [err, value] = goTryRaw(() => JSON.parse('{/}')) // err is UnknownError | undefined, value is T | undefined // fetch todos, fallback to empty array, send error to your error tracking service const [err, todos = []] = await goTryRaw(fetchTodos()) // err is UnknownError | undefined if (err) sendToErrorTrackingService(err) ``` ## Advanced Usage ### Sequential Async Operations Chain multiple async operations with clean error handling: ```ts import { goTry } from 'go-go-try' async function fetchUserData(userId: string) { // Fetch user const [fetchErr, user] = await goTry(fetch(`/api/users/${userId}`)) if (fetchErr) return [fetchErr, undefined] as const // Parse response const [parseErr, data] = await goTry(user!.json()) if (parseErr) return [parseErr, undefined] as const // Validate/transform const [validateErr, validated] = goTry(() => validateUser(data!)) if (validateErr) return [validateErr, undefined] as const return [undefined, validated] as const } const [err, user] = await fetchUserData('123') if (err) { console.error('Failed to fetch user:', err) } else { console.log('User:', user) } ``` ### Parallel Execution with `goTryAll` Execute multiple promises in parallel: ```ts import { goTryAll } from 'go-go-try' const [errors, results] = await goTryAll([ fetchUser(userId), fetchPosts(userId), fetchComments(userId) ]) // errors is [string | undefined, string | undefined, string | undefined] // results is [User | undefined, Posts | undefined, Comments | undefined] const [user, posts, comments] = results ``` ### Safe Unwrapping with `goTryOr` Like `goTry`, but returns a default value on failure instead of `undefined`: > **Note:** For static default values, you can use destructuring instead: > ```ts > // These are equivalent for static defaults: > const [err, config = {port: 3000}] = goTry(() => JSON.parse(configString)) > const [err, config] = goTryOr(() => JSON.parse(configString), {port: 3000}) > ``` > Use `goTryOr` when you need **lazy evaluation** (the default is only computed on failure): ```ts import { goTryOr } from 'go-go-try' // ✅ Use goTryOr with a function for lazy evaluation - default only computed on failure const [err, user] = await goTryOr(fetchUser(id), () => ({ id: 'anonymous', name: 'Guest', createdAt: new Date() // This won't run on success })) // ❌ Avoid: wasteful - createDefault() runs even on success const [err, config = createDefault()] = goTry(loadConfig()) // ✅ Better: lazy - createDefault() only runs on failure const [err, config] = goTryOr(loadConfig(), () => createDefault()) ``` ### Express/Fastify Error Handling Use in API route handlers: ```ts import { goTry } from 'go-go-try' import express from 'express' const app = express() app.post('/users', async (req, res) => { const [err, user] = await goTry(createUser(req.body)) if (err) { return res.status(400).json({ error: err }) } res.json(user) }) // Batch endpoint app.post('/batch', async (req, res) => { const [errors, results] = await goTryAll( req.body.operations.map(op => processOperation(op)) ) const hasErrors = errors.some(e => e !== undefined) res.status(hasErrors ? 207 : 200).json({ results, errors: errors.filter(Boolean) }) }) ``` ### Type Guards Narrow types using `isSuccess` and `isFailure`: ```ts import { goTry, isSuccess, isFailure } from 'go-go-try' const result = goTry(() => riskyOperation()) if (isSuccess(result)) { // result[1] is typed as T (not T | undefined) console.log(result[1]) } else if (isFailure(result)) { // result[0] is typed as E (not E | undefined) console.error(result[0]) } ``` You can also narrow types by destructuring and checking the error: ```ts const [err, value] = goTry(() => riskyOperation()) if (err === undefined) { // value is typed as T (not T | undefined) console.log(value) } else { // err is typed as string (not string | undefined) console.error(err) // value is typed as undefined in this branch } ``` ### Tagged Errors for Discriminated Unions Create typed errors with a `_tag` property for pattern matching and discriminated unions: ```ts import { taggedError, goTryRaw, failure, type Result } from 'go-go-try' // Define error types const DatabaseError = taggedError('DatabaseError') const NetworkError = taggedError('NetworkError') const ValidationError = taggedError('ValidationError') // Create a union type import type { TaggedUnion } from 'go-go-try' // Option 1: Using TaggedUnion helper (cleaner) const DatabaseError = taggedError('DatabaseError') const NetworkError = taggedError('NetworkError') const ValidationError = taggedError('ValidationError') type AppError = TaggedUnion<[typeof DatabaseError, typeof NetworkError, typeof ValidationError]> // Equivalent to: DatabaseError | NetworkError | ValidationError // Option 2: Using InstanceType (standard TypeScript) type AppErrorVerbose = | InstanceType<typeof DatabaseError> | InstanceType<typeof NetworkError> | InstanceType<typeof ValidationError> // Use in functions with typed error returns async function fetchUser(id: string): Promise<Result<AppError, User>> { const [dbErr, user] = await goTryRaw(queryDatabase(id), { errorClass: DatabaseError }) if (dbErr) return failure(dbErr) const [netErr, enriched] = await goTryRaw(enrichUserData(user!), { errorClass: NetworkError }) if (netErr) return failure(netErr) return [undefined, enriched] as const } // Pattern matching on errors const [err, user] = await fetchUser('123') if (err) { switch (err._tag) { case 'DatabaseError': console.error('Database failed:', err.message) break case 'NetworkError': console.error('Network issue:', err.message) break case 'ValidationError': console.error('Invalid data:', err.message) break } } // Exhaustive switch with compile-time safety function assertNever(value: never): never { throw new Error(`Unhandled case: ${String(value)}`) } function handleError(err: AppError): string { switch (err._tag) { case 'DatabaseError': return `DB: ${err.message}` case 'NetworkError': return `NET: ${err.message}` case 'ValidationError': return `VAL: ${err.message}` default: // TypeScript will error here if any case is missing above return assertNever(err) } } ``` The `taggedError` function creates an error class with: - `_tag`: A readonly string literal for discriminated unions - `message`: The error message - `cause`: Optional cause for error chaining - `name`: Set to the tag value ### Helper Functions Build custom utilities on top of the primitives: ```ts import { goTry, success, failure, type Result } from 'go-go-try' // Custom validation helper function validateEmail(email: string): Result<string, string> { if (!email.includes('@')) { return failure('Invalid email format') } return success(email.toLowerCase().trim()) } // Usage const [err, normalizedEmail] = validateEmail('User@Example.COM') if (err) { console.error(err) // Doesn't trigger } else { console.log(normalizedEmail) // 'user@example.com' } ``` ## API ### `goTry<T>(value)` Executes a function, promise, or value and returns a Result type with error message as string. ```ts function goTry<T>(value: T | Promise<T> | (() => T | Promise<T>)): Result<string, T> | Promise<Result<string, T>> ``` ### `goTryRaw<T, E>(value, options?)` Like `goTry` but returns the raw Error object instead of just the message. By default, errors are wrapped in `UnknownError` (a tagged error class). You can customize this behavior with the options object: ```ts type GoTryRawOptions<E> = | { errorClass: ErrorConstructor<E> } // Wrap ALL errors in this class | { systemErrorClass: ErrorConstructor<E> } // Only wrap non-tagged errors | {} // Use defaults ``` > **Note:** `errorClass` and `systemErrorClass` are **mutually exclusive**. TypeScript will show an error if you try to pass both. > - Use `errorClass` when you want all errors wrapped in a specific type > - Use `systemErrorClass` when you want tagged errors to pass through and only wrap untagged errors ```ts // Without options - err is UnknownError | undefined function goTryRaw<T>(value: T | Promise<T> | (() => T | Promise<T>)): Result<UnknownError, T> | Promise<Result<UnknownError, T>> // With options object - err is E | undefined function goTryRaw<T, E>(value: T | Promise<T> | (() => T | Promise<T>), options: GoTryRawOptions<E>): Result<E, T> | Promise<Result<E, T>> ``` **Examples:** ```ts const DatabaseError = taggedError('DatabaseError') const NetworkError = taggedError('NetworkError') // Default - errors wrapped in UnknownError const [err1, data1] = await goTryRaw(fetchData()) // err1 is UnknownError | undefined // err1?._tag === 'UnknownError' // Options object - wrap all errors const [err2, data2] = await goTryRaw(fetchData(), { errorClass: DatabaseError }) // err2 is DatabaseError | undefined // err2?._tag === 'DatabaseError' // Options object - systemErrorClass only wraps non-tagged errors const [err3, data3] = await goTryRaw(fetchData(), { systemErrorClass: NetworkError }) // If fetchData throws a tagged error (e.g., DatabaseError), it passes through // If fetchData throws a plain Error, it gets wrapped in NetworkError ``` ### `goTryAll<T>(items, options?)` Executes multiple promises or factory functions with optional concurrency limit. Returns a tuple of `[errors, results]` with fixed tuple types preserving input order. ```ts interface GoTryAllOptions { concurrency?: number // 0 = unlimited (default), 1 = sequential, N = max concurrent } function goTryAll<T extends readonly unknown[]>( items: { [K in keyof T]: Promise<T[K]> | (() => Promise<T[K]>) }, options?: GoTryAllOptions ): Promise<[{ [K in keyof T]: string | undefined }, { [K in keyof T]: T[K] | undefined }]> ``` **Promise mode** (pass promises directly): ```ts // Run all in parallel (default): const [errors, results] = await goTryAll([ fetchUser(1), // Promise<User> fetchUser(2), // Promise<User> fetchUser(3), // Promise<User> ]) // errors: [string | undefined, string | undefined, string | undefined] // results: [User | undefined, User | undefined, User | undefined] ``` **Factory mode** (pass functions that return promises): ```ts // True lazy execution - factories only called when a slot is available const [errors, results] = await goTryAll([ () => fetchUser(1), // Only called when concurrency slot available () => fetchUser(2), // Only called when concurrency slot available () => fetchUser(3), // Only called when concurrency slot available () => fetchUser(4), // Only called when concurrency slot available ], { concurrency: 2 }) // Use factory mode when you need to: // - Rate limit API calls (don't start HTTP requests until allowed) // - Control database connection limits // - Limit expensive computation resources ``` ### `goTryAllRaw<T, E>(items, options?)` Like `goTryAll`, but returns raw Error objects instead of error messages. Non-tagged errors are wrapped in `UnknownError` by default (consistent with `goTryRaw`). Tagged errors pass through unchanged. Supports the same `errorClass` and `systemErrorClass` options as `goTryRaw`, plus `concurrency` control. ```ts interface GoTryAllRawOptions<E> { concurrency?: number // errorClass and systemErrorClass are mutually exclusive errorClass?: ErrorConstructor<E> // Wrap ALL errors systemErrorClass?: ErrorConstructor<E> // Only wrap non-tagged errors } function goTryAllRaw<T extends readonly unknown[], E = UnknownError>( items: { [K in keyof T]: Promise<T[K]> | (() => Promise<T[K]>) }, options?: GoTryAllRawOptions<E> ): Promise<[{ [K in keyof T]: E | undefined }, { [K in keyof T]: T[K] | undefined }]> ``` **Example:** ```ts const DatabaseError = taggedError('DatabaseError') // Default behavior - non-tagged errors wrapped in UnknownError const [errors1] = await goTryAllRaw([ fetchUser(1), fetchUser(2), ]) // Wrap all errors in DatabaseError const [errors2] = await goTryAllRaw([ fetchUser(1), fetchUser(2), ], { errorClass: DatabaseError }) // Combine concurrency control with errorClass const [errors3] = await goTryAllRaw([ () => fetchUser(1), () => fetchUser(2), () => fetchUser(3), ], { concurrency: 2, errorClass: DatabaseError }) ``` ### `goTryOr<T>(value, defaultValue)` Like `goTry`, but returns a default value on failure instead of `undefined`. The default can be either a static value or a function (for lazy evaluation). ```ts function goTryOr<T>(value: T | Promise<T> | (() => T | Promise<T>), defaultValue: T | (() => T)): Result<string, T> | Promise<Result<string, T>> ``` ### `isSuccess(result)` / `isFailure(result)` Type guards to check result status. ```ts function isSuccess<E, T>(result: Result<E, T>): result is Success<T> function isFailure<E, T>(result: Result<E, T>): result is Failure<E> ``` ### `success(value)` / `failure(error)` Helper functions to create Result tuples. ```ts function success<T>(value: T): Success<T> function failure<E>(error: E): Failure<E> ``` ### `taggedError<T>(tag)` Creates a tagged error class for discriminated error handling. Returns a class constructor that extends `Error` and includes a readonly `_tag` property. ```ts function taggedError<T extends string>(tag: T): TaggedErrorClass<T> // Returned class interface: class TaggedErrorClass<T> extends Error implements TaggedError<T> { readonly _tag: T readonly cause?: unknown constructor(message: string, options?: { cause?: unknown }) } ``` **Example:** ```ts const DatabaseError = taggedError('DatabaseError') const err = new DatabaseError('connection failed', { cause: originalError }) console.log(err._tag) // 'DatabaseError' console.log(err.message) // 'connection failed' console.log(err.name) // 'DatabaseError' console.log(err.cause) // originalError ``` ### `UnknownError` A default tagged error class used by `goTryRaw` when no options are provided, or when `systemErrorClass` is not specified in the options object. ```ts import { UnknownError, goTryRaw } from 'go-go-try' // Errors are automatically wrapped in UnknownError const [err, data] = goTryRaw(() => { throw new Error('something went wrong') }) if (err) { console.log(err._tag) // 'UnknownError' console.log(err.message) // 'something went wrong' console.log(err.cause) // original Error } ``` Since `UnknownError` is the default for `systemErrorClass`, you can distinguish between known tagged errors and unexpected system errors without specifying any options: ```ts const DatabaseError = taggedError('DatabaseError') function fetchData() { // Operations that might throw DatabaseError should use errorClass to wrap them const [err1, data1] = goTryRaw(() => queryDatabase(), { errorClass: DatabaseError }) // Other operations use the default behavior - non-tagged errors become UnknownError const [err2, data2] = goTryRaw(() => parseData(data1)) // err2 is UnknownError | undefined // Now you can distinguish between known and unknown error types if (err1) { console.log('Known DB error:', err1.message) } else if (err2) { console.log('Unexpected error:', err2.message) } } ``` ### `TaggedUnion<T>` Creates a union type from multiple tagged error classes. ```ts type TaggedUnion<T extends readonly ErrorConstructor<unknown>[]> = { [K in keyof T]: T[K] extends ErrorConstructor<infer E> ? E : never }[number] ``` **Example:** ```ts const DatabaseError = taggedError('DatabaseError') const NetworkError = taggedError('NetworkError') const ValidationError = taggedError('ValidationError') // Before (verbose): type AppErrorVerbose = | InstanceType<typeof DatabaseError> | InstanceType<typeof NetworkError> | InstanceType<typeof ValidationError> // After (clean): type AppError = TaggedUnion<[typeof DatabaseError, typeof NetworkError, typeof ValidationError]> // Results in: DatabaseError | NetworkError | ValidationError ``` #### Automatic Union Inference When using `goTryRaw` with different error classes in the same function, TypeScript **automatically infers** the union type without needing explicit type annotations: ```ts // No explicit return type needed! async function fetchUserData(id: string) { // First operation might fail with DatabaseError const [dbErr, user] = await goTryRaw(queryDb(id), { errorClass: DatabaseError }) if (dbErr) return failure(dbErr) // returns Failure<DatabaseError> // Second operation might fail with NetworkError const [netErr, enriched] = await goTryRaw(enrichUser(user!), { errorClass: NetworkError }) if (netErr) return failure(netErr) // returns Failure<NetworkError> return success(enriched) // returns Success<User> } // TypeScript infers: Promise<Result<DatabaseError | NetworkError, User>> // No TaggedUnion or explicit types needed! ``` The inferred union enables exhaustive pattern matching: ```ts const [err, user] = await fetchUserData('123') if (err) { switch (err._tag) { case 'DatabaseError': /* handle db error */ break case 'NetworkError': /* handle network error */ break default: assertNever(err) // compile-time safety } } ``` ## Types ```ts type Success<T> = readonly [undefined, T] type Failure<E> = readonly [E, undefined] type Result<E, T> = Success<T> | Failure<E> // Error type helpers type TaggedInstance<T> = T extends ErrorConstructor<infer E> ? E : never type TaggedUnion<T extends readonly ErrorConstructor<unknown>[]> = { [K in keyof T]: T[K] extends ErrorConstructor<infer E> ? E : never }[number] // Options for goTryRaw (errorClass and systemErrorClass are mutually exclusive) type GoTryRawOptions<E> = | { errorClass: ErrorConstructor<E>; systemErrorClass?: never } | { errorClass?: never; systemErrorClass: ErrorConstructor<E> } | { errorClass?: never; systemErrorClass?: never } // Options for goTryAllRaw (includes concurrency + error class options) type GoTryAllRawOptions<E> = | { concurrency?: number; errorClass: ErrorConstructor<E>; systemErrorClass?: never } | { concurrency?: number; errorClass?: never; systemErrorClass: ErrorConstructor<E> } | { concurrency?: number; errorClass?: never; systemErrorClass?: never } ``` ## License MIT