drizzle-team / drizzle-orm

Headless TypeScript ORM with a head. Runs on Node, Bun and Deno. Lives on the Edge and yes, it's a JavaScript ORM too 😅
https://orm.drizzle.team
Apache License 2.0
23.27k stars 567 forks source link

Add proper error handling and wrapping #376

Open dankochetov opened 1 year ago

kibertoad commented 1 year ago

What is missing?

probablykasper commented 1 year ago

Is this for returning errors, so we can prevent bugs from forgetting to handle exceptions and from incorrectly guessing the error type?

fermentfan commented 1 year ago

This would be super helpful for our code quality. I noticed this when Drizzle threw me an error, when I tested our API with a wrong resource ID to test if the 404 works. Postgres was already mad, because the wrong ID wasn't a valid UUIDv4. Catching this now and throwing an appropriate 404 is kinda smelly.

Error codes would be nice to have!

swelham commented 10 months ago

Another example of how error handling could improve code quality is with errors raised from constraints.

For example, when see a unique containst error, we get a fairly raw looking error object in our try..catch(e).

{
  code: '23505'
  column: undefined
  constraint: 'users_email_index'
  dataType: undefined
  detail: 'Key (email)=(user@example.com) already exists.'
  file: 'nbtinsert.c'
  hint: undefined
  internalPosition: undefined
  internalQuery: undefined
  length: 231
  line: '664'
  name: 'error'
  position: undefined
  routine: '_bt_check_unique'
  schema: 'public'
  severity: 'ERROR'
  table: 'users'
  where: undefined
  message: 'duplicate key value violates unique constraint "users_email_index"'
  stack: <stacktrace_goes_here>
}

We then need to check that certain properties exist with specific values to determine what type of error it was.

A few options are:

// Check the constraint name or message
e.constraint === 'users_email_index';
//or
e.message === 'duplicate key value violates unique constraint "users_email_index"';

// Checking the detail field with a regex
const rx = new RegExp(/Key \(email\).*already exists/);
rx.test(e.detail);

// Checking more internal specifics
e.routine === '_bt_check_unique';

It would be really nice to have some higher level type that hides the internals and allows for simpler use within a catch statement.

algora-pbc commented 10 months ago

💎 $50 bounty created by @john-griffin 👉 To claim this bounty, submit your pull request on Algora 📝 Before proceeding, please make sure you can receive payouts in your country 💵 Payment arrives in your account 2-5 days after the bounty is rewarded 💯 You keep 100% of the bounty award 🙏 Thank you for contributing to drizzle-team/drizzle-orm!

rishi-raj-jain commented 10 months ago

What's the exactly scope of this? @john-griffin

kesh-007 commented 10 months ago

Is the issue still open? I am interested in working on this.

swelham commented 10 months ago

Hey. Yes this is still open.

From this issue we (@john-griffin and myself) are hoping to achieve a higher level error to work with that has better support for typescript. For example, looking at my comment above, all of the examples rely on checking strings for certain values in a way that doesn't allow us to capture typos at compile time.

I don't have a firm solution and maybe the drizzle team can make some recommendations here, but if we could achieve a type safe way to check for expected database errors, that would achieve the scope of the bounty.

Here are some rough ideas that we have discussed, but are by no means prescriptive.

// Generic error class
class QueryError extends Error {
  type: "NonNull" | "UniqueConstraint" | "CheckConstraint" | "ForeignKey" // etc...
  table: string; // Can this be typed to all known tables
  field: string; // Can this by typed to all known fields
  message: string;
}

// or

// Error type specific error classes
class UniqueConstraintError extends Error {
  table: string; // Can this be typed to all known tables
  field: string; // Can this by typed to all known fields
  message: string;
}
class NonNullError extends Error {
  table: string; // Can this be typed to all known tables
  field: string; // Can this by typed to all known fields
  message: string;
}

Something like this would allow checking for errors without dealing directly with the error class from the pg module and could potentially allow better typing.

catch (e) {
  if (e instanceof QueryError) {
    if (e.type === 'unique' and e.field === 'email') { ... }
    if (e.type === 'unknown_type' and e.field === 'email') { ... } // < type error
  }
}

// or

catch (e) {
  if (e instanceof UniqueConstraintError) {
    if (e.field === 'email') { ... }
    if (e.field === 'unknown_field') { ... } // < type error
  }  
}

Again, happy to defer to the drizzle team for API specific guidance.

Angelelz commented 10 months ago

Those working on this one should do the slash command for algora. Screenshot 2023-11-17 100843

Neeraj319 commented 8 months ago

add error classes fast bro

afogel commented 6 months ago

any updates on where this is prioritized in roadmap? Seems like someone might be open to working on it, as long they get guidance from drizzle team on what would work best for drizzle's API? @Angelelz

dinogomez commented 6 months ago

Bumping @afogel 's question, good error handling would be great for displaying the errors on the forms.

StepanMynarik commented 5 months ago

Yet another unique constraint user here. Very much +1

panthyy commented 5 months ago

+1

nantokaworks commented 5 months ago

+1

ThomasAunvik commented 4 months ago

+1 It would really help alot with debugging and error reporting if you had a usable stacktrace. Had a time figuring out why that it was my /[anything]/layout.tsx was causing the issue, and not the /layout.tsx or the /(main)/layout.tsx.

 ⨯ PostgresError: invalid input syntax for type uuid: "some-404-page"
    at Socket.emit (node:events:519:28)
digest: "2286078463"
 GET /some-404-page 500 in 97ms
afogel commented 4 months ago

@Angelelz just a gentle bump reminder here -- any additional guidance the team can give here?

creadevv commented 4 months ago

For the ones having issues with invalid uuids, I have kind of a work around. I am using uuid_v1(), and those strings are always 32 letters/numbers and 4 hyphens, so 36 characters.

I am using Sveltekit and put this in my +page.server.ts:

 if (params.uuid.length !== 36) {
    throw error(404, 'Not found here');
}

const [event] = await db.select().from(eventTable).where(eq(eventTable.uuid, params.uuid));

if (!event) {
    throw error(404, 'Not found here');
}

You can even add some regex to check if there is a combination of 4 hyphens and 32 letters/numbers.

The best solution would still be a way to handle those errors.

cellulosa commented 3 months ago

I'm working with superforms and trpc. For the moment I'm handling errors like so:

// +page.server.ts

export const actions: Actions = {
    default: async ({ request, fetch }) => {
        const form = await superValidate(request, zod(schema));

        if (!form.valid) {
            return fail(400, { form });
        }

        try {
            const trpc = trpcOnServer(fetch);
            const [newClient] = await trpc.clients.save.mutate({ name: form.data.clientName });
            setMessage(form, `The project was created correctly.`);

        } catch (error) {
            if (error instanceof TRPCError || error instanceof TRPCClientError) {
                if (error.message == 'duplicate key value violates unique constraint "client_name_unique"')
                    setError(form, 'clientName', 'Client name already exists.');
            } else {
                setMessage(form, 'Could not create the project.', {
                    status: 400
                });
            }
        }

        return { form };
    }
};

but would be good to be able to avoid hardcoding it

jonnicholson94 commented 2 months ago

+1 on this - would be great!

RobertMercieca commented 2 months ago

+1 would indeed be nice to have

cybercoder-naj commented 1 month ago

+1 love to see good error handling from drizzle

thebergamo commented 3 weeks ago

It would be really good to have at least some simple examples on error handling in the docs.

probablykasper commented 3 weeks ago

I use this helper function to help alleviate the pain:

type QueryError = Error & { code?: unknown }

export async function query<D, P extends QueryPromise<D>>(promise: P) {
    try {
        const result = await promise
        return { data: result, error: null }
    } catch (e) {
        if (e instanceof Error) {
            return { data: null, error: e as QueryError }
        } else {
            throw e
        }
    }
}

Example usage:

const { data, error } = await query(
    db.insert(users).values({
        email,
    }),
)
if (error?.code === '23505' && error.message === 'duplicate key value violates unique constraint "users_email_unique"') {
    return fail(400, 'Email already in use')
} else if (!data) {
    throw error
}
console.log(data)
ayoubkhial commented 4 days ago

We can also rely on the error code property:

Unique constraint:

try {
  return await this.db.insert(schema.users).values(user).returning();
} catch (error) {
  const code: string = error?.code;
  // Unique constraint:
  if (code === '23505') {
    const detail: string = error?.detail;
    const matches = detail.match(/\((.*?)\)/g);
    if (matches) {
      const [key, value] = matches.map((match) => match.replace(/\(|\)/g, ''));
      throw new BadRequestException(`An entry with the value '${value}' already exists for the field '${key}'.`);
    }
  } 
  // Not-Null constraint
  else if (code === '23502') {
    const table: string = error?.table;
    const column: string = error?.column;
    throw new BadRequestException(`The column '${column}' in table '${table}' cannot be null.`);
  }
}

But for other validation constraints, like VARCHAR(50), the error details are a bit limited:

{
  message: value too long for type character varying(50)
  length: 98,
  severity: 'ERROR',
  code: '22001',
  detail: undefined,
  hint: undefined,
  position: undefined,
  internalPosition: undefined,
  internalQuery: undefined,
  where: undefined,
  schema: undefined,
  table: undefined,
  column: undefined,
  dataType: undefined,
  constraint: undefined,
  file: 'varchar.c',
  line: '638',
  routine: 'varchar'
}