YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
General▾
Interface▾
Observability▾
Performance▾
Native▾
Development▾

Error Handling

An order that is already paid cannot be edited, and the user should hear why in their own language. In Akan the server throws a dictionary key, and the client translates that key and shows it.
One error travels through four steps:
order.dictionary.ts
1. Declare: register each error sentence as an [en, ko] pair in .error({}).
order.document.tsorder.service.ts
2. Throw: when a business rule fails, throw new Err("order.error.notDraft").
HTTP · WebSocket
3. Send: the server answers with the untranslated key, a status code and data.
order.store.ts
4. Show: fetch restores it as Err, and the store action shows it as a translated toast.

Declare Errors

Start in the module's dictionary. The keys you declare in .error({}) are the only keys Err accepts, so TypeScript catches a typo.
The snippet leaves out the other stages of the chain:
apps/myapp/lib/order/order.dictionary.ts
  • The key path is fixed. notDraft in the order dictionary is thrown as order.error.notDraft.
  • Placeholders. {productName} is filled from the data you throw with, as shown in Use Data below.
  • Korean ends in '다.' Write each Korean .error() sentence as a full sentence ending in 다.
  • Success messages are not errors. A toast such as order.addItemSuccess is declared in .translate({}).

Throw Err

Throw Err when a business rule the user can understand and fix fails. A rule about the document's own state belongs in its document method, so every service shares the same check.
Only a draft order can change its title:
apps/myapp/lib/order/order.document.ts
Where each rule lives
order.document.ts
A precondition on the document's own state, such as only a draft order being editable.
order.service.ts
A rule that loads or compares other documents, such as the product having to exist.
order.signal.ts
Who may call the endpoint at all, such as only the order's owner, written as a guard.
Where to import Err
*.document.ts*.service.ts*.signal.ts
Server files import it from the module's dict barrel.
*.tsx
UI files import it from the app's client entry; a lib uses @libs/<lib>/client.
common/**env/**
No import path for Err exists here, so keep throwing code out of these folders.

Choose Status

new Err() answers with status 400. When the HTTP meaning matters, throw a named helper instead; each one takes the same arguments as new Err().
new Err(key)400
The default: a business rule rejected the request.
Err.BadRequest400
The same 400, named explicitly.
Err.Unauthorized401
The caller has not signed in or proven who they are.
Err.Forbidden403
The user is known but may not do this action.
Err.NotFound404
The requested record does not exist.
Err.Conflict409
The current state cannot accept this action.
In a service, the order's state and the product's existence pick different statuses:
apps/myapp/lib/order/order.service.ts
  • get throws, load returns null. getOrder(id) throws a plain error when the record is missing, so the caller sees a generic 500.
  • Answer a missing record yourself. When a user can reach an id that does not exist, call loadProduct(id) and throw Err.NotFound on null.
  • It is the HTTP status too. The response goes out with the same code, so proxies and access logs also see 404 or 409.

Use Data

Pass data when the translated sentence needs values. The server keeps the dictionary key in error and sends data beside it for interpolation.
The second argument of Err is that data object:
apps/myapp/lib/order/order.document.ts
  • Names match the placeholders. data.productName fills {productName} in every language's sentence.
  • Strings and numbers only. The error toast fills only string and number values and drops anything else.
  • A missing value stays visible. A placeholder with no value is shown as written, like {quantity}, so the gap reads as a bug rather than as content.

Client Handling

fetch restores an error response as Err, and every store action already runs inside a wrapper that catches it. The wrapper translates the key into the user's language and shows a toast, so an action holds only the happy path, with no try/catch.
The store action calls the endpoint and handles success only:
apps/myapp/lib/order/order.store.ts
The button knows nothing about failure. It calls the action and lets the wrapper answer:
apps/myapp/lib/order/Order.Util.tsx
  • Toast, then rethrow. After the toast the wrapper throws the error again, so code after await st.do.addItemToOrder() does not run when it fails.
  • Network failures are covered. A timeout, an unreachable server or a restarting one arrives as an Err with a base.error.* key and gets the same toast.
  • Check input with msg.error. For a client-side check before the call, run msg.error("<key>") and return early; never throw.
  • try/finally is for spinners. UI code may use it to reset a spinner; catching belongs to the wrapper.

Response Shape

HTTP and websocket errors carry almost the same fields. You rarely build this by hand, but knowing it makes debugging easier.
The stockNotEnough error from above arrives like this:
error
The dictionary key you threw, never a translated sentence.
statusCode
400 by default or the helper's status, and the HTTP response carries the same one.
data
The placeholder values, present only when you passed them.
details
Extra debugging detail, present only when set.
path
The endpoint path, sent over HTTP only and never in a websocket error frame.
timestamp
When the server answered, as an ISO string.
When a plain Error escapes
Anything that is not an Err is answered as a 500, so the user never sees the dictionary sentence:
ErrErr.*
Answered with its own statusCode, and error holds the dictionary key.
ErrorgetOrder(missingId)
Answered as 500, and a deployed build sets error to Internal Server Error.
  • In development you see everything. Under akan start the response carries the real message and the stack.
  • The stack is in the server log. Every such 500 is logged with its stack, even when the response hides it.
  • Debugging a deployed build. Set AKAN_ERROR_DETAIL=1 to put the real message back into the response.

Tips

  • Name keys by domain and reason. For example order.error.notDraft, order.error.stockNotEnough, user.error.wrongPassword.
  • Do not translate on the server. Send the key and data, and let the client pick the user's language.
  • Pass a remote Err through as is. A server-to-server fetch.x(…, { origin }) restores the remote Err, so rethrow it rather than wrapping it in new Error or a key of your own.

Released under the MIT License

Connect your AI to these docs

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js All rights reserved.System managed bybassman