> For the complete documentation index, see [llms.txt](https://emeditweb.gitbook.io/pulsar-stellar-sdk/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://emeditweb.gitbook.io/pulsar-stellar-sdk/concepts/error-handling.md).

# Error handling

The SDK has two ways to tell you something did not produce a value: it returns `null`, or it throws. They mean different things, and the split is consistent across every method.

* **`null` is an expected absence.** The call worked. The answer is that the thing you asked for is not there.
* **A throw is a failure.** The call could not be completed: a bad input, an unreachable server, a response the SDK could not trust.

Absence is always `null`, never `undefined`. A missing contract, an absent HTTP status, and an empty search all say so with `null`, so you branch on `=== null` without a second check.

## What returns null

| Method                 | Returns null when                                                             |
| ---------------------- | ----------------------------------------------------------------------------- |
| `getContract(id)`      | the indexer is not tracking that contract (a 404 with a `not_found` envelope) |
| `event(id)`            | the indexer has no event with that id (same well-formed 404)                  |
| `findPulsarError(err)` | the cause chain holds no Pulsar error                                         |

A 404 counts as absence only when it carries the indexer's `not_found` envelope. A bare 404, the kind a proxy or a misrouted request produces, is not a well-formed absence and throws instead. That distinction is ADR-019.

Two absences that are deliberately not `null`:

* **`listContracts()` returns `[]`, not null**, when the indexer tracks nothing. The indexer answered; the answer is an empty set. Absence of a resource and absence of contents are different facts.
* **`events(id)` on an untracked contract throws**, it does not return an empty page. "Not indexed" and "no matching events" are different facts, and the most common integration mistake, querying a contract nobody registered, should not look like a valid empty result. That is ADR-021.

## The error hierarchy

Everything the SDK throws descends from `PulsarError`, so you branch on the kind with `instanceof` rather than matching message text.

```
PulsarError (abstract, never thrown directly)
├─ PulsarNetworkError   the request did not produce a usable response
└─ PulsarValidationError a value did not match its schema
```

`PulsarError` is abstract. Catch it to handle every SDK failure at once, but you will always receive one of the two concrete subclasses. Each carries the `operation` that failed and a frozen `details` object, and its `toString` includes both, so one log line is actionable.

**`PulsarNetworkError`** covers an unreachable indexer or RPC endpoint, a timeout, a non-success HTTP status, an unreadable body, and the indexer's error envelope. It adds two fields:

* `status`: the HTTP status, or `null` when the request never produced one (a DNS failure, a timeout before any response).
* `url`: the URL that was called, or `null` when the failure preceded the request.

**`PulsarValidationError`** covers input the SDK rejects and a server response that does not match the shape this SDK version expects. It exposes `issues`, the array of Zod issues, so you can report the offending field rather than the whole message.

```ts
import { PulsarNetworkError, PulsarValidationError } from '@pulsar-stellar/sdk';

try {
  const page = await client.events(contractId);
  // ...
} catch (error) {
  if (error instanceof PulsarNetworkError) {
    console.error('request failed', error.status, error.url, error.operation);
  } else if (error instanceof PulsarValidationError) {
    for (const issue of error.issues) {
      console.error(issue.path.join('.'), issue.message);
    }
  } else {
    throw error; // not ours, do not swallow it
  }
}
```

A validation error can come from either side of a call. A malformed contract id is caught before any request goes out. A response that is JSON but the wrong shape is caught when it arrives. Both are `PulsarValidationError`, and the `operation` field tells you which call was in flight.

## Recovering a wrapped error

If your own code wraps an SDK error in another error, the type check above stops working, because the thing you catch is yours, not the SDK's. `findPulsarError` walks the `cause` chain and returns the first Pulsar error in it, or `null` if there is none. The walk is cycle-safe, so a self-referential chain terminates rather than looping.

```ts
import { findPulsarError } from '@pulsar-stellar/sdk';

catch (error) {
  const pulsar = findPulsarError(error);
  if (pulsar !== null) {
    console.error(pulsar.operation, pulsar.details);
  }
}
```

## ping reports bad news without throwing

`ping` is the one place where a bad answer is still a successful call. A reachable indexer that reports `ok: false` resolves to a `PingResult` with `ok: false`. The request worked; the news was bad. `ping` throws only when it cannot get a usable response at all: the indexer is unreachable, times out, returns a non-success status, or answers with something that is not the health shape.

```ts
const health = await client.ping(); // resolves even when unhealthy
if (!health.ok) {
  console.warn('indexer reachable but reports unhealthy', health.version);
}
```

This keeps "the indexer is down" (a throw) distinct from "the indexer is up and telling you it has a problem" (a resolved `ok: false`), which are different operational situations that deserve different handling.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://emeditweb.gitbook.io/pulsar-stellar-sdk/concepts/error-handling.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
