Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proper error handling and wrapping #376

Open
dankochetov opened this issue Apr 4, 2023 · 45 comments
Open

Add proper error handling and wrapping #376

dankochetov opened this issue Apr 4, 2023 · 45 comments
Assignees
Labels
enhancement New feature or request has-pr This issue has one or more PRs that that could close the issue when merged

Comments

@dankochetov
Copy link
Contributor

No description provided.

@dankochetov dankochetov converted this from a draft issue Apr 4, 2023
@github-project-automation github-project-automation bot moved this from Backlog to Done in Public Roadmap Apr 4, 2023
@dankochetov dankochetov added the enhancement New feature or request label Apr 4, 2023
@dankochetov dankochetov reopened this Apr 4, 2023
@dankochetov dankochetov moved this from Done to Backlog in Public Roadmap Apr 4, 2023
@kibertoad
Copy link

What is missing?

@probablykasper
Copy link

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

@fermentfan
Copy link

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
Copy link

swelham commented Nov 2, 2023

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)=([email protected]) 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
Copy link

💎 $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
Copy link

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

@kesh-007
Copy link

kesh-007 commented Nov 16, 2023

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

@swelham
Copy link

swelham commented Nov 17, 2023

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
Copy link
Collaborator

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

@Neeraj319
Copy link

add error classes fast bro

@afogel
Copy link

afogel commented Feb 28, 2024

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
Copy link

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

@StepanMynarik
Copy link

Yet another unique constraint user here. Very much +1

@bytekai
Copy link

bytekai commented Apr 10, 2024

+1

1 similar comment
@nantokaworks
Copy link

+1

@ThomasAunvik
Copy link

ThomasAunvik commented Apr 24, 2024

+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
Copy link

afogel commented Apr 25, 2024

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

@creadevv
Copy link

creadevv commented May 15, 2024

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
Copy link

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
Copy link

+1 on this - would be great!

@RobertMercieca
Copy link

+1 would indeed be nice to have

@cybercoder-naj
Copy link

+1 love to see good error handling from drizzle

@thebergamo
Copy link

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

@probablykasper
Copy link

probablykasper commented Aug 25, 2024

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
Copy link

ayoubkhial commented Sep 13, 2024

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'
}

@kedom1337
Copy link

+1 much needed feature

@hi-reeve
Copy link

+1 for this

@waynehamadi
Copy link

waynehamadi commented Nov 15, 2024

I ended creating my own middleware for drizzle, and it gives me a lot of control, including the ability to finally see errors in my application code without having to put try catch on all my queries:
https://gist.github.com/waynehamadi/571b1e9a6d00adcae33fd25eccf3833a

the code is very opinionated for a remix/express app, so really just to give inspirations more than a clear guide

@felixgeissler
Copy link

+1 here as well.

@elieven
Copy link

elieven commented Dec 1, 2024

bump

@wmtrinu
Copy link

wmtrinu commented Dec 2, 2024

+1, I need to do proper error handling,thanks!

@murolem
Copy link

murolem commented Dec 9, 2024

bump

@Naveenravi07
Copy link

+1

2 similar comments
@aparx
Copy link

aparx commented Dec 28, 2024

+1

@omar-tabbush
Copy link

+1

@mkpanq
Copy link

mkpanq commented Jan 4, 2025

Just started to create logic for handle errors from drizzle in my app, so...

+1

@thanhlq
Copy link

thanhlq commented Jan 7, 2025

+1

1 similar comment
@Ericnr
Copy link

Ericnr commented Jan 7, 2025

+1

@stefanprobst
Copy link

please stop the "+1" comments and use the thumbs-up reactions button. thanks.

@L-Mario564 L-Mario564 self-assigned this Jan 20, 2025
@RickVM
Copy link

RickVM commented Jan 23, 2025

For those using postgres,we use the following workaround that might be of some inspiration for implementing your own (workaround)

Here's how I built something to identify uniqueConstraintErrors.

import pg from 'postgres';

const isUniqueConstraintError = (error: unknown): boolean => {
  /** https://github.com/porsager/postgres/pull/901 */
  // eslint-disable-next-line import/no-named-as-default-member
  return error instanceof pg.PostgresError && error.code === '23505';
};

export default isUniqueConstraintError;
import isUniqueConstraintError from '@/lib/database/errors/isUniqueConstraintError';

// Insert item into db
try {
  // Insert item into the database with the given data and max position
  await db.insert(cars).values({ ...data });
} catch (error) {
  // Check if the error is a unique constraint error
  if (isUniqueConstraintError(error)) {
    // handle however you want....
  } else {
    // Handle other errors
    ....
  }
}

Ofcourse, you can use the PostgresError to get more details.

@L-Mario564
Copy link
Collaborator

I'm beginning work on this issue. This is by no means the finalized API, but here's what I got at the moment:

import { is } from 'drizzle-orm';
import { PgQueryError, ERROR } from 'drizzle-orm/pg-core';

try {
  await db.insert(users).values({ email: '[email protected]' })
} catch (err) {
  if (is(err, PgQueryError)) {
    // This is a PG error. This class applies regardless of driver being used

   if (err.code === ERROR.INTEGRITY_CONSTRAINT_VIOLATION.UNIQUE_VIOLATION) {
     // Unique constraint violation error
    error(401, 'a user with that email address already exists'); 
   }
  } else {
    // Some other kind of error
  }
}

The PgQueryError class would contain all the details present in the native error thrown by the DB, alongside some additional utils like getting the column names of the relevant columns (for integrity constraint violations) and the query build by Drizzle with its params.

Some possible additions:

const db = drizzle(postgres, {
  onError: (err) => { /* Do stuff */ }
})

Any feedback is welcome.

@probablykasper
Copy link

probablykasper commented Jan 23, 2025

With that implementation, I'll write something like this when I wanna return the value and error:

import { is } from 'drizzle-orm';
import { PgQueryError } from 'drizzle-orm/pg-core';

// Use .catch() instead of try...catch so we keep the type of the returned data
const result = await db.select().from(users).catch((error) => {
  // Since there's no type safety here, you have to guess the error type. If you
  // forget to do the check correctly, or if you miss that a future change, your
  // users will hopefully let you know
  if (is(error, PgQueryError)) {
    // Since we're in a closure, we need to return it here so we can return it again later
    return { error };
  } else {
    // IMPORTANT, DO NOT FORGET: `error` can be anything, so make sure you don't
    // silently ignore it
    console.error('Unexpected error', error);
    throw error;
  }
})
if ('error' in result) {
  return { error: result.error };
}
return result;

I would much prefer error-by-value:

// result type: { data: T, error: null } | { data: null, error: { ... } }
const result = await db.select().from(users);
if (result.error) {
	return { error: result.error};
}
return result.data;

@L-Mario564
Copy link
Collaborator

L-Mario564 commented Jan 24, 2025

With that implementation, I'll write something like this when I wanna return the value and error:

That would be a big breaking change for Drizzle's API, not to mention that error can't be fully type safe, as you may encounter Drizzle errors, driver errors, vendor-specific DB errors, standard DB errors, Node/Bun/Deno/Hermes errrors, etc., many of which aren't documented to begin with.

Maybe we could implement an API like that via an abstraction provided by Drizzle, but that'd be a separate thing and won't consider this within the scope of this issue, since you can make your own abstractions anyways.

@probablykasper
Copy link

Since the issue is about proper error handling, I was hoping that meant moving away from error-guessing. In my opinion it's important for writing reliable software, especially when dealing with databases. Should I open a new issue about it?

not to mention that error can't be fully type safe

I'd say that error-by-value is always type-safe (*by TypeScript standards). Unexpected errors should still be exceptions, like panics in other languages. If a dependency throws an exception that Drizzle can't reliably know the type of, it can have a type like for example { code: 'pg_core', details: unknown }

@darrenkeen
Copy link

For those using postgres,we use the following workaround that might be of some inspiration for implementing your own (workaround)

Here's how I built something to identify uniqueConstraintErrors.

import pg from 'postgres';

const isUniqueConstraintError = (error: unknown): boolean => {
/** porsager/postgres#901 */
// eslint-disable-next-line import/no-named-as-default-member
return error instanceof pg.PostgresError && error.code === '23505';
};

export default isUniqueConstraintError;

import isUniqueConstraintError from '@/lib/database/errors/isUniqueConstraintError';

// Insert item into db
try {
// Insert item into the database with the given data and max position
await db.insert(cars).values({ ...data });
} catch (error) {
// Check if the error is a unique constraint error
if (isUniqueConstraintError(error)) {
// handle however you want....
} else {
// Handle other errors
....
}
}

Ofcourse, you can use the PostgresError to get more details.

Weirdly, error instanceof pg.PostgresError returns false for me. Can't see severity_local property in my error 🤔 Supabase not returning what a normal postgres DB would perhaps?

@L-Mario564 L-Mario564 added the has-pr This issue has one or more PRs that that could close the issue when merged label Feb 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request has-pr This issue has one or more PRs that that could close the issue when merged
Projects
Status: Backlog
Development

No branches or pull requests