Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quick-002-sync-async-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@power-rent/try-catch": minor
---

allow running both sync and async functions
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
dist
.worktrees
18 changes: 10 additions & 8 deletions .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ See: .planning/PROJECT.md (updated 2026-01-31)
Phase: 1 of 3 (Core Try Semantics)
Plan: 0 of TBD in current phase
Status: Ready to plan
Last activity: 2026-01-31 - Completed quick task 001: write tests that verify type safety accoriding to the use cases covered in Readme.md
Last activity: 2026-02-01 - Completed quick task 002: stabilize Try typecheck failures

Progress: [░░░░░░░░░░] 0%

## Performance Metrics

**Velocity:**
- Total plans completed: 1
- Average duration: 2m 41s
- Total execution time: 2m 41s
- Total plans completed: 2
- Average duration: 4m 10s
- Total execution time: 8m 19s

**By Phase:**

| Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------|
| Quick | 1 | 2m 41s | 2m 41s |
| Quick | 2 | 8m 19s | 4m 10s |

**Recent Trend:**
- Last 5 plans: -
Expand All @@ -42,7 +42,8 @@ Progress: [░░░░░░░░░░] 0%
Decisions are logged in PROJECT.md Key Decisions table.
Recent decisions affecting current work:

- None yet.
- Use return-type generics to preserve async/sync inference in Try
- Treat never as non-promise in IfPromise to avoid type collapse

### Pending Todos

Expand All @@ -61,9 +62,10 @@ None yet.
| # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------|
| 001 | write tests that verify type safety accoriding to the use cases covered in Readme.md | 2026-01-31 | 9595583 | [001-write-tests-that-verify-type-safety-acco](./quick/001-write-tests-that-verify-type-safety-acco/) |
| 002 | stabilize Try typecheck failures for type-safety tests | 2026-02-01 | 793eed1 | [002-tests-are-failing](./quick/002-tests-are-failing/) |

## Session Continuity

Last session: 2026-01-31 14:21 UTC
Stopped at: Completed quick-001 type-safety-tests plan 001
Last session: 2026-02-01 06:36 UTC
Stopped at: Completed quick-002 tests-are-failing plan 01
Resume file: None
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,20 @@ The library accepts any parameter types as function arguments:

```typescript
// String parameters
const greeting = await new Try(greet, 'Alice', 'Hi').value();
const greeting = new Try(greet, 'Alice', 'Hi').value();

// Number parameters
const sum = await new Try(add, 5, 3).unwrap();
const sum = new Try(add, 5, 3).unwrap();

// Mixed parameter types
const message = await new Try(formatMessage, 123, 'Test message', true).value();
const message = new Try(formatMessage, 123, 'Test message', true).value();

// No parameters
const timestamp = await new Try(getCurrentTime).value();
const timestamp = new Try(getCurrentTime).value();
```

Sync functions return values immediately; async functions requires `await`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

### Advanced Usage

```typescript
Expand Down Expand Up @@ -233,22 +235,24 @@ Enable debug logging to console. When enabled, errors will be logged to console.

### Execution Methods

#### `.unwrap(): Promise<Awaited<T>>`
#### `.unwrap(): T | Promise<Awaited<T>>`

Execute the function and return the result. Throws the original error if one occurred. Will mask the error message if `.report('custom message')` is called in the chain.

#### `.default<Return>(defaultValue: Return): Try<T, TArgs>`

Set a default value that will be returned by `.value()` when an exception occurs.

#### `.value(): Promise<Awaited<T> | Return | undefined>`
#### `.value(): T | undefined | Promise<Awaited<T> | undefined>`

Execute the function and return the result, the configured default value, or `undefined` if an error occurs.

#### `.error(): Promise<Error | undefined>`
#### `.error(): Error | undefined | Promise<Error | undefined>`

Execute the function and return the error if one occurred, or `undefined` if successful.

Sync functions return values immediately; async functions return Promises.

## Examples

### Different Parameter Types
Expand All @@ -258,20 +262,20 @@ Execute the function and return the error if one occurred, or `undefined` if suc
function greet(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}
const greeting = await new Try(greet, 'Alice', 'Hi').value();
const greeting = new Try(greet, 'Alice', 'Hi').value();

// Number parameters
function add(a: number, b: number): number {
return a + b;
}
const sum = await new Try(add, 5, 3).value();
const sum = new Try(add, 5, 3).value();

// Mixed parameter types
function formatMessage(id: number, message: string, urgent: boolean): string {
const prefix = urgent ? '[URGENT]' : '[INFO]';
return `${prefix} #${id}: ${message}`;
}
const formatted = await new Try(formatMessage, 123, 'System error', true)
const formatted = new Try(formatMessage, 123, 'System error', true)
.report('Message formatting failed')
.tag('component', 'notification')
.default('Unexpected error')
Expand All @@ -281,7 +285,7 @@ const formatted = await new Try(formatMessage, 123, 'System error', true)
function getCurrentTime(): number {
return Date.now();
}
const timestamp = await new Try(getCurrentTime).value();
const timestamp = new Try(getCurrentTime).value();

// Object parameters (key extraction available)
const user = await new Try(fetchUser, { userId: 123, includeProfile: true })
Expand Down
56 changes: 56 additions & 0 deletions src/__tests__/Try.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,62 @@ describe('Try', () => {
expect(Sentry.addBreadcrumb).not.toHaveBeenCalled();
});

describe('sync return behavior', () => {
it('returns sync value immediately (no Promise)', () => {
const result = new Try(() => 'ok').value();

expect(result).toBe('ok');
expect(result).not.toBeInstanceOf(Promise);
});

it('returns default value on sync error', () => {
const result = new Try(() => {
throw new Error('boom');
})
.default('fallback')
.value();

expect(result).toBe('fallback');
});

it('captures sync error for error() and result()', () => {
const error = new Try(() => {
throw new Error('boom');
}).error();

const result = new Try(() => {
throw new Error('boom');
}).result();

expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe('boom');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.message).toBe('boom');
}
});

it('throws on unwrap for sync error', () => {
expect(() =>
new Try(() => {
throw new Error('boom');
}).unwrap(),
).toThrow('boom');
});

it('runs finally immediately for sync path and returns Try instance', () => {
const finallySpy = vi.fn();
const tryInstance = new Try(() => 'ok');

const chained = tryInstance.finally(finallySpy);
const value = tryInstance.value();

expect(chained).toBe(tryInstance);
expect(value).toBe('ok');
expect(finallySpy).toHaveBeenCalledTimes(1);
});
});

it('should throw an error', async () => {
const params = { parameterKey: 'alpha', parameterKey1: 'beta' };

Expand Down
10 changes: 9 additions & 1 deletion src/__tests__/type-safety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,37 @@ describe('Try README type safety', () => {
const syncValue = new Try(formatMessage, 1, 'Test', true).value();

expectTypeOf(asyncValue).toEqualTypeOf<Promise<User | undefined>>();
expectTypeOf(syncValue).toEqualTypeOf<Promise<string | undefined>>();
expectTypeOf(syncValue).toEqualTypeOf<string | undefined>();
});

it('narrows value() return when default() is provided', () => {
const withDefault = new Try(fetchUser, { id: 123 }).default(null).value();
const syncDefault = new Try(formatMessage, 1, 'Test', true)
.default('fallback')
.value();

expectTypeOf(withDefault).toEqualTypeOf<Promise<User | null>>();
expectTypeOf(syncDefault).toEqualTypeOf<string>();
});

it('keeps error() typed as Error | undefined', () => {
const errorValue = new Try(fetchUser, { id: 123 })
.report('Failed to fetch user')
.error();
const syncError = new Try(formatMessage, 1, 'Test', true).error();

expectTypeOf(errorValue).toEqualTypeOf<Promise<Error | undefined>>();
expectTypeOf(syncError).toEqualTypeOf<Error | undefined>();
});

it('keeps unwrap() typed as Awaited<T>', () => {
const receipt = new Try(chargeCard, { amount: 1000, currency: 'USD' })
.report('Payment failed')
.unwrap();
const syncUnwrap = new Try(formatMessage, 1, 'Test', true).unwrap();

expectTypeOf(receipt).toEqualTypeOf<Promise<Receipt>>();
expectTypeOf(syncUnwrap).toEqualTypeOf<string>();
});

it('validates breadcrumbs keys against object parameter types', () => {
Expand Down
Loading