Effect-TS / effect

An ecosystem of tools to build robust applications in TypeScript
https://effect.website
MIT License
7.4k stars 231 forks source link

Strange behavior of the final type of effect errors #3742

Closed MrOxMasTer closed 1 week ago

MrOxMasTer commented 1 week ago

What version of Effect is running?

3.8.4

What steps can reproduce the bug?

When creating a similar function:

import { DrizzleError } from 'drizzle-orm';
import { DatabaseError } from 'pg';

class CustomError extends Error {
  // readonly _tag = 'CustomError';
}

export class UserNotFoundError extends Error {
  readonly _tag = 'userNotFoundError';
}

export const getUserByEmail = (email: string) =>
  Effect.gen(function* (_) {
    const db = yield* _(DBClient);

    const user = yield* _(
      pipe(
        Effect.tryPromise({
          try: () =>
            db.query.users.findFirst({
              where: (users, { eq }) => eq(users.email, email),
            }),
          catch: (_error) =>
            new DrizzleError({
              message: `Something went wrong with the database: ${_error}`,
            }),
          // new CustomError(
          //   `Something went wrong with the database: ${_error}`,
          // ),
         // new DatabaseError(
         //   `Something went wrong with the database: ${_error}`,
         //  1,
         //   'error',
            ),
        }),
      ),
    );

    if (!user) {
      return yield* _(Effect.fail(new UserNotFoundError('User not found')));
    }

    return yield* _(Effect.succeed(user));
  }).pipe(Effect.provideService(DBClient, db));

const res = getUserByEmail('');

The type of error returned by the effect will be different depending on the transmission of the error

What is the expected behavior?

The final effect will be 2 errors instead of 1

What do you see instead?

Throwing out one of the errors, moreover, which has a _tag: image

Additional information

if, for example, you put customError without _tag, then this will be output: image

If you add _tag to `customError', then the expected behavior will be: image

If you change it to the error DatabaseError, then this will also work correctly: image

I do not understand by what logic it determines which errors will fall into the final effect and which will not, given that the standard DatabaseError does not have a '_tag`

it's strange, considering that all these errors come from the basic Error, but some of them fall into the final effect, and some do not

MrOxMasTer commented 1 week ago

Can you explain why some types are included in the final type and others are not?

mikearnaldi commented 1 week ago

the issue is when one error is a subtype of another, for example:

class MyError1 extends Error {}
class MyError2 extends Error {}

you'd have that MyError1 === MyError2 at the type level, due to structural equivalence, in this case TypeScript will reduce MyError1 | MyError2 | Error to Error.

The default strategy in effect is to use tagged errors so:

class MyError1 extends Data.TaggedError("MyError1")<{}> {}
class MyError2 extends Data.TaggedError("MyError2")<{}> {}

that are no longer equal at the type level

mikearnaldi commented 1 week ago

Can you explain why some types are included in the final type and others are not?

(was writing...)

MrOxMasTer commented 1 week ago

you'd have that MyError1 === MyError2 at the type level, due to structural equivalence, in this case TypeScript will reduce MyError1 | MyError2 | Error to Error.

If you look at the pictures, it is clearly shortened not to Error, but to everything in the world except it.

And why exactly do those errors that are more correctly written get thrown out?

MrOxMasTer commented 1 week ago

you'd have that MyError1 === MyError2 at the type level, due to structural equivalence, in this case TypeScript will reduce MyError1 | MyError2 | Error to Error.

Well, clearly, this is a typescript problem, that it shortens so much, and _tag I took it from old videos, I thought it was the right thing to do, but it's still not very convenient when instead of an existing error and its extension, you need to create a completely new one from some thing from the library

MrOxMasTer commented 1 week ago

It just doesn't look intuitive when some error goes to the final, and some doesn't, and it doesn't matter if it has one or not and what laws it is guided by, if they all come from Error

mikearnaldi commented 1 week ago

I was just explaining how subtyping work, in your precise case CustomError is the most common given that CustomError is equal to Error and all the other errors extends Error.

This is not a typescript problem, it is how structural typing work which is the basic of TypeScript.

You'd need custom errors anyway to be able to catch them nicely with catchTag

MrOxMasTer commented 1 week ago

The default strategy in effect is to use tagged errors so:

class MyError1 extends Data.TaggedError("MyError1")<{}> {}
class MyError2 extends Data.TaggedError("MyError2")<{}> {}

Why is the question if this is the current strategy, then why do the first links in the documentation talk about the old strategy: https://effect.website/docs/guides/error-management/expected-errors

MrOxMasTer commented 1 week ago

https://effect.website/docs/guides/error-management/yieldable-errors

mikearnaldi commented 1 week ago

The default strategy in effect is to use tagged errors so:

class MyError1 extends Data.TaggedError("MyError1")<{}> {}
class MyError2 extends Data.TaggedError("MyError2")<{}> {}

Why is the question if this is the current strategy, then why do the first links in the documentation talk about the old strategy: https://effect.website/docs/guides/error-management/expected-errors

all the examples contain a _tag, it's even highlighted. We don't want to introduce too many concepts at once