Error handling

When a plain error is thrown in a loader, action, or server$ function, the request aborts and a sanitized 500 error is returned to the client. The full error is visible during development, but the details are hidden in production. Qwik provides the tools necessary to customise how errors are handled.

Throwing a ServerError instance (or the requestEvent.error() helper, which creates one) allows you to abort the request on purpose with a different status code and serialised data — the nearest error page renders with that status.

NOTEThrowing is for aborting to the error page. For expected failures that the page should display inline (form validation, domain rules), return requestEvent.fail(status, data) instead — the failure surfaces on the reactive loader.error / action.error and the page keeps rendering. See routeLoader$ and routeAction$.
// Throw ServerErrors from a routeLoader$
const useProduct = routeLoader$(async (ev) => {
  const product = await fetch('api/product/1')
 
  if (!product) {
    // Throw a 404 with a custom payload — the error page renders with a 404 status
    throw new ServerError(404, 'Product not found')
 
    // Or use the existing helper function
    throw ev.error(404, 'Product not found')
  }
 
  return product
})
 
// Throw ServerErrors from a server$
const getPrices = server$(() => {
  if (!isAuthenticated()) {
    throw new ServerError(401, { code: 401 })
  }
 
  return fetch('api/product/1/prices')
})
 
export default component$(() => {
  const product = useProduct()
 
  useVisibleTask$(() => {
    getPrices()
      .then()
      .catch(err => {
        // The payload from a ServerError is deserialised as the error caught in the client
        if (err.code === 401) {
          // Navigate to login page
        }
 
        // Show generic error
      })
  })
 
  return <div>Product page</div>
})

Error interceptor

Intercepting errors with middleware has a few usecases: you might want to hide error details in production systems, add structured error logging, or map the error status codes from RPC API calls to HTTP status codes. This is all achieveable with middleware in a plugin file.

Thrown errors — throw error(...), thrown ServerErrors, and unexpected errors — propagate up through next(), so middleware can catch them. Note that a returned fail(...) is not an exception: it becomes the loader/action .error state and never passes through the interceptor.

// src/routes/plugin@errors.ts
import { type RequestHandler } from '@qwik.dev/router'
import { RedirectMessage, ServerError } from '@qwik.dev/router/middleware/request-handler'
import { isDev } from '@qwik.dev/core/build'
 
export const onRequest: RequestHandler = async ({ next }) => {
  try {
    return await next();
  } catch (err) {
    // Pass through 3xx redirects
    if (err instanceof RedirectMessage) {
      throw err
    }
 
    // Pass through ServerErrors
    if (err instanceof ServerError) {
      throw err
    }
 
    // Log unknown errors
    console.error('unknown error', err)
 
    if (isDev) {
      throw err
    } else {
      throw new ServerError(500, 'Internal server error');
    }
  }
};
 
 
 

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • DustinJSilk