Basics of Selective Error Handling

1. What Is Selective Error Handling?

Selective error handling means:

Handle errors you know how to handle, and let unexpected errors propagate.

Instead of catching everything:

try {
  await doSomething();
} catch (error) {
  console.error(error);
}

you distinguish between different kinds of errors.

Expected error

Handle it

Unexpected error

Let it propagate

2. Why Not Catch Everything?

This is dangerous:

try {
  const data = await getUser();
  const result = await process(data);
  await save(result);
} catch (error) {
  return "Something went wrong";
}

You don’t know what failed.

It could be:

Treating all of these the same makes debugging harder.


3. Handle Known Errors

Create specific error types:

class NotFoundError extends Error {
  constructor(message) {
    super(message);
    this.name = "NotFoundError";
  }
}

Throw it:

if (!user) {
  throw new NotFoundError("User not found");
}

Then selectively handle it:

try {
  const user = await getUser(id);
} catch (error) {
  if (error instanceof NotFoundError) {
    return null;
  }

  throw error;
}

The important part:

throw error;

Unknown errors continue upward.


4. Example

try {
  const user = await getUser(id);

  return user;
} catch (error) {
  if (error instanceof NotFoundError) {
    return null;
  }

  throw error;
}

Behavior:

NotFoundError

return null

DatabaseError

throw

ProgrammingError

throw

5. HTTP Errors

Selective handling is especially useful with fetch().

const res = await fetch("/api/users");

if (res.status === 404) {
  // Expected
  return [];
}

if (!res.ok) {
  throw new Error(`Request failed: ${res.status}`);
}

const data = await res.json();

Here:

404

Handle normally

500

Throw

Network failure

fetch() throws

6. Multiple Known Errors

You can handle several specific cases:

try {
  const res = await fetch("/api/user");

  if (res.status === 404) {
    return null;
  }

  if (res.status === 401) {
    throw new UnauthorizedError();
  }

  if (!res.ok) {
    throw new Error("Request failed");
  }

  return await res.json();
} catch (error) {
  if (error instanceof UnauthorizedError) {
    return redirect("/login");
  }

  throw error;
}

7. Don’t Check Error Strings

Avoid:

catch (error) {
  if (error.message === "User not found") {
    // ...
  }
}

Error messages are meant for humans and can change.

Prefer:

if (error instanceof NotFoundError) {
  // ...
}

8. Custom Error Classes

A common structure:

class AppError extends Error {
  constructor(message) {
    super(message);
    this.name = "AppError";
  }
}

class NotFoundError extends AppError {
  constructor(message = "Not found") {
    super(message);
    this.name = "NotFoundError";
  }
}

class UnauthorizedError extends AppError {
  constructor(message = "Unauthorized") {
    super(message);
    this.name = "UnauthorizedError";
  }
}

Now you can distinguish errors:

try {
  await operation();
} catch (error) {
  if (error instanceof NotFoundError) {
    // handle
  } else if (error instanceof UnauthorizedError) {
    // handle
  } else {
    throw error;
  }
}

9. Selective Handling in Astro

For an Astro API route:

export async function GET() {
  try {
    const user = await getUser();
    return Response.json(user);
  } catch (error) {
    if (error instanceof NotFoundError) {
      return Response.json({ error: "User not found" }, { status: 404 });
    }

    console.error(error);

    return Response.json({ error: "Internal Server Error" }, { status: 500 });
  }
}

The API handles:

NotFoundError → 404

while unexpected errors become:

Unknown error → 500

10. An Important Pattern

A good general structure:

try {
  // operation
} catch (error) {
  if (error instanceof KnownError) {
    // handle known error
  }

  throw error;
}

Or, at the application’s outer boundary:

try {
  // application code
} catch (error) {
  if (error instanceof KnownError) {
    // convert to appropriate response
  } else {
    // log unexpected error
    console.error(error);

    // generic 500 response
  }
}

11. Error Handling Layers

Think of errors moving upward:

Database

Service

API route

HTTP response

Each layer should handle errors it understands.

Database layer

Knows:

DatabaseError

Service layer

Knows:

UserNotFoundError
PermissionError

API layer

Converts them into:

404
403
500

12. Don’t Handle Errors Too Early

Bad:

async function getUser(id) {
  try {
    return await db.getUser(id);
  } catch (error) {
    return null;
  }
}

Now the caller can’t distinguish:

User doesn't exist

from:

Database crashed

Better:

async function getUser(id) {
  return await db.getUser(id);
}

Let the error propagate.

The API layer can decide how to respond.


13. finally

Use finally for cleanup that should happen regardless of success or failure:

try {
  await operation();
} catch (error) {
  console.error(error);
  throw error;
} finally {
  cleanup();
}

Flow:

success ─────┐

          finally

error ───────┘

14. The Core Rule

┌─────────────────────────┐
│ Did I expect this error?│
└────────────┬────────────┘

       ┌─────┴─────┐
       │           │
      YES          NO
       │           │
       ↓           ↓
   Handle it     Throw it

Remember