SuperAPI AI Directory

error-handling

Coding & development

View repository

Updated 10-16 02:37

About this skill

Error Handling — CAP Best Practices

Primary reference: https://cap.cloud.sap/docs/node.js/events#errors Messages: https://cap.cloud.sap/docs/node.js/events#req-messages

The four messaging methods

// REJECT — stops processing, returns error HTTP response
req.reject(422, 'Quantity {0} exceeds available stock {1}', [qty, stock])

// WARN — non-blocking, adds to @SAP.Messages OData annotation
req.warn(200, 'STOCK_LOW', [product.title])

// INFO — informational message added to response
req.info(200, 'Order will be shipped by {0}', [shipDate])

// ERROR — accumulates error, does NOT stop processing (use reject for that) req.error(400, 'Field {0} is required', ['title']) // ... then call: req.reject() // throws all accumulated errors at once


## HTTP status codes — when to use what

| Code | When |
|------|------|
| 400 | Bad request / validation failure (malformed input) |
| 401 | Not authenticated |
| 403 | Authenticated but not authorized |
| 404 | Entity not found |
| 409 | Conflict (e.g. duplicate, already processed) |
| 422 | Unprocessable entity (semantic/business rule failure) |
| 500 | Unexpected server error (use sparingly, prefer specific codes) |

## i18n error messages

Define messages in `_i18n/messages.properties`:
```properties
ORDER_CLOSED=Order {0} is already closed
STOCK_INSUFFICIENT=Insufficient stock: requested {0}, available {1}
DUPLICATE_ORDER=An order for product {0} already exists today

Reference by key in handlers:

req.reject(409, 'ORDER_CLOSED', [req.data.orderID])
req.reject(422, 'STOCK_INSUFFICIENT', [qty, stock])

CAP resolves the key using the user's locale automatically.

Structured errors with targets

Target a specific field in OData error responses (Fiori shows inline validation):

req.reject({
  code: 422,
  message: 'Price must be positive',
  target: 'price',         // field name in the entity
  status: 422
})

// Or with req.error for accumulation:
req.error({ code: 400, message: 'Title is required', target: 'title' })
req.error({ code: 400, message: 'Price is required', target: 'price' })
req.reject()  // throws all at once

Global error handler

Register in server.js or your AppService:

cds.on('error', (err, req) => {
  // Log all errors centrally
  console.error(`[${req?.user?.id}] ${err.code}: ${err.message}`)
  // You can mutate the error before it's sent to the client
  if (err.status === 500) {
    err.message = 'An internal error occurred. Please contact support.'
  }
})

Error handling for remote service calls

try {
  const result = await S4.run(SELECT.from('A_BusinessPartner').where({ BusinessPartner: id }))
  return result
} catch (err) {
  if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
    req.reject(503, 'Backend system is currently unavailable')
  } else if (err.status === 404) {
    req.reject(404, 'Business partner {0} not found in S/4HANA', [id])
  } else {
    // Re-throw unexpected errors (they'll become 500)
    throw err
  }
}

Custom error class

For reusable domain errors:

class BusinessError extends cds.error {
  constructor(code, message, args = []) {
    super(message, { code, status: 422 })
    this.messageArgs = args
  }
}

// Usage:
throw new BusinessError('ORDER_CLOSED', 'Order {0} is already closed', [id])

Common mistakes to avoid

  • ❌ Using throw new Error(...) directly — bypasses CAP's error formatting
  • ❌ Returning error objects instead of calling req.reject() — agent gets no error
  • ❌ Sending stack traces to clients in production
  • ❌ Using status 500 for business rule violations (use 409/422)
  • ❌ Forgetting target on validation errors — Fiori Elements needs it for inline display
  • ❌ Swallowing errors in remote service calls (silent failures)