26 · Error Handling

Use req.reject() vs req.error(), return structured errors with target fields, and wire i18n error message keys.

Episode 26 — Error Handling & Custom Errors

What you'll learn

Why this matters

A consistent error API is a quality signal. Fiori apps expect @odata.error formatted responses; REST clients expect structured JSON. CAP gives you primitives to produce both from one place, with target-field-level errors that map directly to Fiori input field highlights.

Error primitives

req.reject() — abort immediately

this.before('CREATE', 'Books', (req) => {
  if (!req.data.title)
    return req.reject(400, 'Book must have a title')
  // chain stops here; nothing else runs
})

req.error() — accumulate, then abort

this.before('CREATE', 'Books', (req) => {
  if (!req.data.title)
    req.error(400, 'Title is required', 'title')      // target = field name
  if (req.data.price < 0)
    req.error(400, 'Price must be positive', 'price')
  // Both errors collected; CAP auto-rejects with all messages after BEFORE phase
})

Structured error with target

req.error({
  code:    'INVALID_PRICE',
  message: 'Price must be between {0} and {1}',
  args:    [0, 9999],
  target:  'price',           // maps to Fiori field highlighting
  status:  400
})

OData error response

{
  "error": {
    "code":    "INVALID_PRICE",
    "message": "Price must be between 0 and 9999",
    "target":  "price",
    "@Common.numericSeverity": 4,
    "details": []
  }
}

Error status codes

Status Use case
400 Bad request / validation failure
401 Not authenticated
403 Authenticated but not authorized
404 Entity not found
409 Conflict (e.g., already processed)
422 Semantic error (valid syntax, invalid business rule)
500 Unexpected server error

Throwing errors in async code

this.on('submitOrder', async (req) => {
  const order = await SELECT.one.from('Orders').where({ ID: req.data.orderId })
  if (!order) return req.reject(404, `Order not found: ${req.data.orderId}`)

  try {
    await externalService.notify(order)
  } catch (e) {
    // Log internally, return friendly message to client
    console.error('Notification failed:', e)
    return req.reject(502, 'Failed to notify external service. Please retry.')
  }
})

i18n error messages

# _i18n/messages.properties
ORDER_NOT_FOUND=Order {0} not found
PRICE_INVALID=Price must be between {0} and {1}
req.reject(404, 'ORDER_NOT_FOUND', [req.data.orderId])
//              ↑ i18n key            ↑ substitution args

CAP resolves the key from _i18n/messages_<locale>.properties based on Accept-Language.

Global error handler

// Catch all unhandled errors across the service
this.on('error', (err, req) => {
  console.error(`[${req.event}] ${req.target?.name}:`, err.message)
  // Optionally transform error before it's sent
  if (err.code === 'SQLITE_CONSTRAINT') {
    err.status  = 409
    err.message = 'A duplicate entry already exists'
  }
})

Error format — REST vs OData

CAP automatically formats errors based on the Accept header:

Accept: application/json        → OData error format
Accept: application/json;odata  → same
Content-Type: application/json  → same
// OData format (Fiori-compatible)
{
  "error": {
    "code":    "400",
    "message": "Title is required",
    "target":  "title"
  }
}

Testing errors

const { POST } = cds.test('.').in(__dirname + '/..')

it('rejects book without title', async () => {
  const { status, data } = await POST('/catalog/Books', { price: 9.99 })
  expect(status).toBe(400)
  expect(data.error.message).toMatch(/title/i)
  expect(data.error.target).toBe('title')
})

it('returns 404 for missing order', async () => {
  const { status } = await POST('/orders/submitOrder', { orderId: 'non-existent' })
  expect(status).toBe(404)
})

Hands-on exercise

  1. Add validation in a BEFORE handler: reject if price > 10000, with target 'price'
  2. Add multi-field validation with req.error(): validate both title and price in one pass
  3. Add an i18n message key for the error; test with Accept-Language: de
  4. Add a global this.on('error', ...) handler that logs all errors with timestamp

Checkpoint ✓

Sign in for progress tracking & Q&A →