---
area: "Learn"
parent: "Advanced rendering chapter"
url: "https://unpoly.com/learn/error-responses"
---
<nav aria-label="Unpoly docs">[All docs](https://unpoly.com/index.md) · [Advanced rendering chapter](https://unpoly.com/learn/advanced-rendering.md)</nav>

# Handling error responses

When the server responds with an error code, like 422 for an invalid form or 500 for a crash, Unpoly renders the response with a separate set of options. This way a failed form submission re-renders the form with validation errors, instead of showing the next screen. You can also decide which responses count as errors.

<nav aria-label="Contents">

#### Contents

- [Rendering error responses differently](#fail-options)
- [Ignoring HTTP error codes](#ignoring-error-codes)
- [Customizing failure detection](#customizing-failure-detection)
- [Local content cannot fail](#local-content)
- [Detecting an error response programmatically](#detecting-failure)
- [Showing your framework's error page in development](#development-error-pages)
- [See also](#see-also)

</nav>

## Rendering error responses differently {#fail-options}

Any HTTP status other than 2xx or [304](https://unpoly.com/learn/conditional-requests.md#rendering-nothing) makes a response an _error response_. For an error response you can pass different render options than for a successful one.

A common use case is a [form submission](https://unpoly.com/learn/submitting-forms.md). A successful response should show a follow-up screen, but an error response should re-render the form with validation errors. A good HTTP status code for an invalid form submission is [422](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422) (Unprocessable Content).

To use a different [render option](https://unpoly.com/api/up.render.md) for an error response, prefix the option with `fail`:

```js
up.render({
  url: '/action',
  method: 'post',
  target: '.content',            // when submission succeeds update '.content'
  failTarget: 'form',            // when submission fails update the form
  scroll: 'auto',                // when submission succeeds use default scroll behavior
  failScroll: '.errors',         // when submission fails scroll to the error messages
  onRendered: () => { ... },     // when submission succeeds run this callback
  onFailRendered: () => { ... }, // when submission fails run this other callback
})
```

Event handlers still begin with `on`, so `{ onRendered }` becomes `{ onFailRendered }`.

When using Unpoly's HTML attributes with [links](https://unpoly.com/api/up-follow.md) or [forms](https://unpoly.com/api/up-submit.md), infix the attribute with `fail`:

```text
<form method="post" action="/action"
  up-submit                 <!-- have Unpoly handle the form -->
  up-target=".content"      <!-- when submission succeeds update '.content' -->
  up-fail-target="form"     <!-- when submission fails update the form -->
  up-scroll="auto"          <!-- when submission succeeds use default scroll behavior -->
  up-fail-scroll=".errors"> <!-- when submission fails scroll to the error messages -->
  ...
</form>
```

> #### Tip
>
> When Unpoly handles a form, the default `[up-fail-target]` is the form itself. This makes sure that any validation errors appear within the `<form>` element, so you rarely need to set the attribute. See [validating forms](https://unpoly.com/learn/validation.md) for examples.

### Which options have a fail variant {#fail-variants}

Options that are used before the request is made, like `{ url, method, confirm }`, have no `fail`-prefixed variant.

Some options, like `{ history, fallback }`, are used for both successful and error responses, but may be overridden with a fail-prefixed variant, e.g. `{ history: true, failHistory: false }`.

Options related to layers, scrolling or focus are never shared. An error response uses `{ failLayer, failScroll, failFocus }`, or the [configured defaults](https://unpoly.com/api/up.fragment.config.md#config.navigateOptions) when these are not given.

### Fail targets for links {#links}

Unlike a form, a link has no natural fragment to show an error in. When a link receives an error response, Unpoly updates the [main fragment](https://unpoly.com/api/main.md) instead of the link's `[up-target]`. This way a server error page shows up in the main fragment, where the user expects it.

To re-render a different fragment, set an `[up-fail-target]` attribute.

A plain [`up.render()`](https://unpoly.com/api/up.render.md) call has no default fail target. When the server responds with an error and you passed neither `{ failTarget }` nor `{ fallback }`, the render pass rejects with an `up.CannotMatch` error.

## Ignoring HTTP error codes {#ignoring-error-codes}

With `{ fail: false }` or `[up-fail=false]` Unpoly will always consider the response to be successful, even with an HTTP 4xx or 5xx status code:

```html
<a href="/missing-page" up-target=".content" up-fail="false">Show page</a>
```

The error page is now rendered into `.content`, as if the server had responded with `200 OK`.

## Customizing failure detection {#customizing-failure-detection}

By default only the status code decides whether a response is an error response. You can also fail a response if it contains a given header or body text.

The following configuration fails all responses with an `X-Unauthorized` header:

```js
let badStatus = up.network.config.fail
up.network.config.fail = (response) => badStatus(response) || response.header('X-Unauthorized')
```

You can also decide to fail a response in an [`up:fragment:loaded`](https://unpoly.com/api/up:fragment:loaded.md) listener:

```js
up.on('up:fragment:loaded', function(event) {
  if (event.response.header('X-Unauthorized')) {
    event.renderOptions.fail = true
  }
})
```

## Local content cannot fail {#local-content}

When the new fragment content is passed as an HTML string (`{ document, fragment, content }`) instead of being requested from a `{ url }`, the update is always considered successful.

## Detecting an error response programmatically {#detecting-failure}

Rendering functions like [`up.render()`](https://unpoly.com/api/up.render.md), [`up.follow()`](https://unpoly.com/api/up.follow.md) or [`up.submit()`](https://unpoly.com/api/up.submit.md) return a promise that rejects when the server sends an error response, or when there is another error.

See [Render error handling example](https://unpoly.com/learn/render-lifecycle.md#error-handling-example).

## Showing your framework's error page in development {#development-error-pages}

In development, many server frameworks show a detailed error page when your code crashes, with a stack trace and sometimes an interactive console. When Unpoly renders that page into a fragment, it loses its own styles and scripts, and parts of it may not be shown at all.

To see the full error page, prevent the [`up:fragment:loaded`](https://unpoly.com/api/up:fragment:loaded.md) event and repeat the request as a full page load:

```js
// Returns true when your development server shows an error page.
function isDevelopmentError(response) {
  return document.body.dataset.environment === 'development'
    && !response.ok
    && /backtrace|stack ?trace|traceback/i.test(response.text)
}

up.on('up:fragment:loaded', function(event) {
  if (isDevelopmentError(event.response)) {
    event.preventDefault()        // don't render the error page into a fragment
    event.request.loadPage()      // repeat the request as a full page load
  }
})
```

The code reads the environment from a `[data-environment]` attribute on the `<body>`. Render that attribute in your layout. In Rails:

```erb
<body data-environment="<%= Rails.env %>">
```

Other frameworks have a similar way to print the current environment into your layout.

[`up.Request#loadPage()`](https://unpoly.com/api/up.Request.prototype.loadPage.md) cannot repeat file uploads, so a form with a file field is sent again without its files.

## See also {#see-also}

When the server responds successfully, but with unexpected content like a login form, your target may be missing from the response. See [Targeting fragments: Dealing with missing targets](https://unpoly.com/learn/targeting-fragments.md#missing-targets).

When a request gets no response at all, because the device is offline or the request timed out, nothing is rendered. See [Handling network issues](https://unpoly.com/learn/network-issues.md).

When a request is aborted, nothing is rendered either. See [Aborting requests: Reacting to aborted requests](https://unpoly.com/learn/aborting-requests.md#reacting).

<nav aria-label="Reading order">

- [Previous: Target derivation](https://unpoly.com/learn/target-derivation.md)
- [Next: Keeping elements](https://unpoly.com/learn/keeping-elements.md)

</nav>

