Skip to content

FAQ ​

Short answers. Each links to the page that treats the question properly.

Why doesn't client.publish() throw? ​

Every fallible operation returns a result instead. Awaiting one gives you a Result you inspect — it does not throw on failure. This is what lets the compiler enumerate what can go wrong.

→ Errors as values

What is the defect branch for? ​

Failures nobody anticipated — a bug such as a payload that cannot be encoded, a broker rejection core cannot classify, or a connection lost while consuming. They are not in the type signature because there is no meaningful way to branch on them. Anticipated failures are modeled and go in errCases: a validation failure (MessageValidationError), a broker that did not take a publish (PublishError), a broker create() could not reach (ConnectionError). Everything else arrives as a defect with a TechnicalError cause.

→ Error model

Why can't I use async in a handler? ​

Because a rejected promise is an untyped exception again, and the worker would have to guess whether to retry. Lifting with fromPromise(promise, qualify) forces you to say what a failure means, which is the decision the worker needs.

→ Errors as values

Which schema libraries can I use? ​

Anything implementing Standard Schema — Zod, Valibot, ArkType. Use Zod unless you have a specific reason not to.

→ Schema libraries

Do I need to install amqplib? ​

No. It ships as a dependency of the amqp-contract packages. You do need unthrown, because it appears in the types you write.

→ Getting started

Why are queues quorum by default? ​

Quorum queues replicate through Raft and survive broker failure. The cost is some write latency, which is almost always worth paying. Classic queues are opt-in for the features quorum does not support: exclusive, autoDelete, and classic maxPriority levels. Plain message priority works on quorum: they prioritise natively on RabbitMQ 4.0+ from each message's priority property, with no queue argument (normal vs high above 4 up to 4.2; 32 strict levels from 4.3).

→ Topology options

Can I change the routing key per publish? ​

No. It comes from the publisher definition. Define a publisher per routing key — that keeps every key a service can emit visible in the contract.

→ Publish messages

Does it support request/reply? ​

Yes. defineRpc declares a request schema, a response schema and optionally a set of typed error codes.

→ Use request/reply

Does it work with NestJS or other frameworks? ​

Yes — it is a plain library with no framework coupling. Create the client and worker wherever your framework builds singletons, and use createContext for per-message dependency injection.

→ Add middleware

Does it support Kafka, NATS or SQS? ​

No. AMQP 0.9.1 only. The generated AsyncAPI document is the only part of a contract that travels to another transport.

→ Comparison

How do non-TypeScript services join in? ​

Generate an AsyncAPI document from the contract and use AsyncAPI's code generators. They get types from the same source of truth, though not the runtime validation.

→ Generate AsyncAPI

How do I test my contracts? ​

@amqp-contract/testing runs your tests against a real RabbitMQ container with an isolated virtual host per test.

→ Test with RabbitMQ

Why did my message go straight to the dead-letter queue without retrying? ​

Either the handler returned a NonRetryableError, or the message failed schema validation. Validation failures bypass retries entirely, because a payload that does not match the schema will not start matching it on a later attempt.

→ The retry model

Why is there no failure reason on my dead-lettered message? ​

Diagnostic headers are only stamped on paths that republish the message. A direct nack — the common case — leaves the message exactly as delivered, and the reason is in the worker's log instead.

→ Retry failed messages

My RPC times out but the server looks healthy. Why? ​

Most often the handler's return value fails the response schema. The worker refuses to publish a malformed reply, so the caller sees a timeout rather than a wrong answer. Check that both sides share the same contract version.

→ Use request/reply

Does a shared connection mean one connection for my whole cluster? ​

No — per process. The cache is a per-process singleton, so each process, worker thread or Lambda instance opens its own.

→ Share connections

Are retries exactly-once? ​

No. Retries are delivery attempts. A handler that charges a card and then fails will charge again on retry. Making the work idempotent is yours.

→ The retry model

Does amqp-contract ever deliver a message twice, even with retries off? ​

Yes. A crash, a dropped connection, or a drain timeout can each produce a duplicate, and none of them need a retry policy to happen.

→ Delivery guarantees

Released under the MIT License.