Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 0 additions & 5 deletions .changeset/thirty-avocados-explain.md

This file was deleted.

8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# neverthrow

## 8.2.0

### Minor Changes

- [#615](https://github.com/supermacro/neverthrow/pull/615) [`85ed7fd`](https://github.com/supermacro/neverthrow/commit/85ed7fd3a1247e4c0e83bba13f5e874282243d75) Thanks [@konker](https://github.com/konker)! - Add orTee, which is the equivalent of andTee but for the error track.

- [#584](https://github.com/supermacro/neverthrow/pull/584) [`acea44a`](https://github.com/supermacro/neverthrow/commit/acea44adb98dda2ca32fe4e882879461cc7cedc2) Thanks [@macksal](https://github.com/macksal)! - Allow ok/err/okAsync/errAsync to accept zero arguments when returning void

## 8.1.1

### Patch Changes
Expand Down
96 changes: 96 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
- [`Result.match` (method)](#resultmatch-method)
- [`Result.asyncMap` (method)](#resultasyncmap-method)
- [`Result.andTee` (method)](#resultandtee-method)
- [`Result.orTee` (method)](#resultortee-method)
- [`Result.andThrough` (method)](#resultandthrough-method)
- [`Result.asyncAndThrough` (method)](#resultasyncandthrough-method)
- [`Result.fromThrowable` (static class method)](#resultfromthrowable-static-class-method)
Expand All @@ -55,6 +56,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
- [`ResultAsync.orElse` (method)](#resultasyncorelse-method)
- [`ResultAsync.match` (method)](#resultasyncmatch-method)
- [`ResultAsync.andTee` (method)](#resultasyncandtee-method)
- [`ResultAsync.orTee` (method)](#resultasyncortee-method)
- [`ResultAsync.andThrough` (method)](#resultasyncandthrough-method)
- [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method)
- [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method)
Expand Down Expand Up @@ -593,6 +595,53 @@ resAsync.then((res: Result<void, ParseError | InsertError>) => {e

---

#### `Result.orTee` (method)

Like `andTee` for the error track. Takes a `Result<T, E>` and lets the `Err` value pass through regardless the result of the passed-in function.
This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging.

**Signature:**

```typescript
class Result<T, E> {
orTee(
callback: (value: E) => unknown
): Result<T, E> { ... }
}
```

**Example:**

```typescript
import { parseUserInput } from 'imaginary-parser'
import { logParseError } from 'imaginary-logger'
import { insertUser } from 'imaginary-database'

// ^ assume parseUserInput, logParseError and insertUser have the following signatures:
// parseUserInput(input: RequestData): Result<User, ParseError>
// logParseError(parseError: ParseError): Result<void, LogError>
// insertUser(user: User): ResultAsync<void, InsertError>
// Note logParseError returns void upon success but insertUser takes User type.

const resAsync = parseUserInput(userInput)
.orTee(logParseError)
.asyncAndThen(insertUser)

// Note no LogError shows up in the Result type
resAsync.then((res: Result<void, ParseError | InsertError>) => {e
if(res.isErr()){
console.log("Oops, at least one step failed", res.error)
}
else{
console.log("User input has been parsed and inserted successfully.")
}
}))
```

[⬆️ Back to top](#toc)

---

#### `Result.andThrough` (method)

Similar to `andTee` except for:
Expand Down Expand Up @@ -1277,6 +1326,53 @@ resAsync.then((res: Result<void, InsertError | NotificationError>) => {e

[⬆️ Back to top](#toc)

---
#### `ResultAsync.orTee` (method)

Like `andTee` for the error track. Takes a `ResultAsync<T, E>` and lets the original `Err` value pass through regardless
the result of the passed-in function.
This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging.

**Signature:**

```typescript
class ResultAsync<T, E> {
orTee(
callback: (value: E) => unknown
): ResultAsync<T, E> => { ... }
}
```

**Example:**

```typescript
import { insertUser } from 'imaginary-database'
import { logInsertError } from 'imaginary-logger'
import { sendNotification } from 'imaginary-service'

// ^ assume insertUser, logInsertError and sendNotification have the following signatures:
// insertUser(user: User): ResultAsync<User, InsertError>
// logInsertError(insertError: InsertError): Result<void, LogError>
// sendNotification(user: User): ResultAsync<void, NotificationError>
// Note logInsertError returns void on success but sendNotification takes User type.

const resAsync = insertUser(user)
.orTee(logUser)
.andThen(sendNotification)

// Note there is no LogError in the types below
resAsync.then((res: Result<void, InsertError | NotificationError>) => {e
if(res.isErr()){
console.log("Oops, at least one step failed", res.error)
}
else{
console.log("User has been inserted and notified successfully.")
}
}))
```

[⬆️ Back to top](#toc)

---
#### `ResultAsync.andThrough` (method)

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "neverthrow",
"version": "8.1.1",
"version": "8.2.0",
"description": "Stop throwing errors, and instead return Results!",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
Expand Down
16 changes: 16 additions & 0 deletions src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
)
}

orTee(f: (t: E) => unknown): ResultAsync<T, E> {
return new ResultAsync(
this._promise.then(async (res: Result<T, E>) => {
if (res.isOk()) {
return new Ok<T, E>(res.value)
}
try {
await f(res.error)
} catch (e) {
// Tee does not care about the error
}
return new Err<T, E>(res.error)
}),
)
}

mapErr<U>(f: (e: E) => U | Promise<U>): ResultAsync<T, U> {
return new ResultAsync(
this._promise.then(async (res: Result<T, E>) => {
Expand Down
25 changes: 25 additions & 0 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,18 @@ interface IResult<T, E> {
*/
andTee(f: (t: T) => unknown): Result<T, E>

/**
* This "tee"s the current `Err` value to an passed-in computation such as side
* effect functions but still returns the same `Err` value as the result.
*
* This is useful when you want to pass the current `Err` value to your side-track
* work such as logging but want to continue error-track work after that.
* This method does not care about the result of the passed in computation.
*
* @param f The function to apply to the current `Err` value
*/
orTee(f: (t: E) => unknown): Result<T, E>

/**
* Similar to `andTee` except error result of the computation will be passed
* to the downstream in case of an error.
Expand Down Expand Up @@ -342,6 +354,10 @@ export class Ok<T, E> implements IResult<T, E> {
return ok<T, E>(this.value)
}

orTee(_f: (t: E) => unknown): Result<T, E> {
return ok<T, E>(this.value)
}

orElse<R extends Result<unknown, unknown>>(
_f: (e: E) => R,
): Result<InferOkTypes<R> | T, InferErrTypes<R>>
Expand Down Expand Up @@ -428,6 +444,15 @@ export class Err<T, E> implements IResult<T, E> {
return err(this.error)
}

orTee(f: (t: E) => unknown): Result<T, E> {
try {
f(this.error)
} catch (e) {
// Tee doesn't care about the error
}
return err<T, E>(this.error)
}

andThen<R extends Result<unknown, unknown>>(
_f: (t: T) => R,
): Result<InferOkTypes<R>, InferErrTypes<R> | E>
Expand Down
50 changes: 50 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,31 @@ describe('Result.Ok', () => {
})
})

describe('orTee', () => {
it('Calls the passed function but returns an original err', () => {
const errVal = err(12)
const passedFn = vitest.fn((_number) => {})

const teed = errVal.orTee(passedFn)

expect(teed.isErr()).toBe(true)
expect(passedFn).toHaveBeenCalledTimes(1)
expect(teed._unsafeUnwrapErr()).toStrictEqual(12)
})
it('returns an original err even when the passed function fails', () => {
const errVal = err(12)
const passedFn = vitest.fn((_number) => {
throw new Error('OMG!')
})

const teed = errVal.orTee(passedFn)

expect(teed.isErr()).toBe(true)
expect(passedFn).toHaveBeenCalledTimes(1)
expect(teed._unsafeUnwrapErr()).toStrictEqual(12)
})
})

describe('asyncAndThrough', () => {
it('Calls the passed function but returns an original ok as Async', async () => {
const okVal = ok(12)
Expand Down Expand Up @@ -1064,6 +1089,31 @@ describe('ResultAsync', () => {
})
})

describe('orTee', () => {
it('Calls the passed function but returns an original err', async () => {
const errVal = errAsync(12)
const passedFn = vitest.fn((_number) => {})

const teed = await errVal.orTee(passedFn)

expect(teed.isErr()).toBe(true)
expect(passedFn).toHaveBeenCalledTimes(1)
expect(teed._unsafeUnwrapErr()).toStrictEqual(12)
})
it('returns an original err even when the passed function fails', async () => {
const errVal = errAsync(12)
const passedFn = vitest.fn((_number) => {
throw new Error('OMG!')
})

const teed = await errVal.orTee(passedFn)

expect(teed.isErr()).toBe(true)
expect(passedFn).toHaveBeenCalledTimes(1)
expect(teed._unsafeUnwrapErr()).toStrictEqual(12)
})
})

describe('orElse', () => {
it('Skips orElse on an Ok value', async () => {
const okVal = okAsync(12)
Expand Down
Loading