A complete guide to designing consistent, developer-friendly error responses for your REST API. Covers RFC 7807 Problem Details, error catalogs, retry logic, and rate limiting.
8. Error Logging and Correlation
Your error responses are the most-read part of your API documentation. When things work, developers don't read docs. When things break, they read error messages.
A good error response:
A bad error response:
{"error": "Bad request"}This tells the developer nothing. They'll file a support ticket. You'll spend 30 minutes asking "which endpoint? what request body?"
A good error response:
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "The 'email' field must be a valid email address.",
"instance": "/users",
"errors": [
{
"field": "email",
"message": "Must be a valid email address",
"value": "not-an-email"
}
]
}RFC 7807 defines a standard format for error responses in HTTP APIs. It was updated by RFC 9457 in 2023.
| Field | Type | Required | Description |
|---|---|---|---|
type | URI | Yes | A URI that identifies the error type. Should point to documentation. |
title | string | Yes | A short, human-readable summary of the error type. |
status | integer | Yes | The HTTP status code. |
detail | string | No | A human-readable explanation specific to this occurrence. |
instance | string | No | A URI that identifies the specific occurrence (e.g., the request path). |
You can add custom fields beyond the standard ones:
| Custom Field | Purpose |
|---|---|
errors | Array of field-level validation errors |
request_id | Correlation ID for debugging |
retry_after | When the client should retry (seconds or ISO 8601 timestamp) |
help_url | Link to relevant documentation |
error_code | Machine-readable error code (e.g., ORDER_NOT_FOUND) |
Error responses using RFC 7807 MUST use the content type:
Content-Type: application/problem+json
This tells clients to parse the response as a Problem Details object, not a regular JSON response.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/insufficient-permissions",
"title": "Insufficient Permissions",
"status": 403,
"detail": "Your API key does not have permission to delete orders. Required scope: orders:delete",
"instance": "/orders/ord_abc123",
"error_code": "INSUFFICIENT_PERMISSIONS",
"required_scope": "orders:delete",
"current_scopes": ["orders:read", "orders:write"]
}Use this structure for ALL error responses:
{
"type": "https://api.example.com/errors/{error-type}",
"title": "Human-Readable Error Title",
"status": 400,
"detail": "Specific explanation of what went wrong for this particular request.",
"instance": "/the/endpoint/that/failed",
"error_code": "MACHINE_READABLE_CODE",
"request_id": "req_abc123xyz",
"timestamp": "2026-06-15T14:30:00Z"
}type -- Should be a stable URI. It doesn't need to resolve to a real page, but it's nice if it does. Use kebab-case for the error slug:
https://api.example.com/errors/validation-errorhttps://api.example.com/errors/rate-limit-exceededhttps://api.example.com/errors/resource-not-foundtitle -- Static per error type. Don't include request-specific details here:
detail -- Dynamic and specific to this request:
error_code -- Machine-readable code for clients to switch on:
VALIDATION_ERROR, RESOURCE_NOT_FOUND, RATE_LIMIT_EXCEEDEDFor malformed requests: invalid JSON, wrong content type, missing required headers.
{
"type": "https://api.example.com/errors/malformed-request",
"title": "Malformed Request",
"status": 400,
"detail": "Request body is not valid JSON. Unexpected token '}' at position 42.",
"error_code": "MALFORMED_JSON"
}Missing or invalid authentication.
{
"type": "https://api.example.com/errors/authentication-required",
"title": "Authentication Required",
"status": 401,
"detail": "The Authorization header is missing or contains an invalid token.",
"error_code": "AUTH_REQUIRED",
"help_url": "https://api.example.com/docs/authentication"
}Always include WWW-Authenticate header:
WWW-Authenticate: Bearer realm="api.example.com", error="invalid_token"Authenticated but not authorized.
{
"type": "https://api.example.com/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "Your API key does not have the 'orders:delete' scope.",
"error_code": "FORBIDDEN",
"required_scope": "orders:delete"
}Resource doesn't exist.
{
"type": "https://api.example.com/errors/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "No order found with ID 'ord_nonexistent'.",
"error_code": "NOT_FOUND",
"resource_type": "order",
"resource_id": "ord_nonexistent"
}State conflicts: duplicates, version mismatches, invalid state transitions.
{
"type": "https://api.example.com/errors/conflict",
"title": "Resource Conflict",
"status": 409,
"detail": "Order ord_abc123 is already shipped and cannot be cancelled.",
"error_code": "INVALID_STATE_TRANSITION",
"current_state": "shipped",
"attempted_transition": "cancelled",
"allowed_transitions": ["delivered", "returned"]
}Business rule violations and validation errors.
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "2 validation errors in request body.",
"error_code": "VALIDATION_ERROR",
"errors": [
{
"field": "email",
"message": "Must be a valid email address",
"code": "INVALID_FORMAT",
"value": "not-an-email"
},
{
"field": "quantity",
"message": "Must be between 1 and 999",
"code": "OUT_OF_RANGE",
"value": 0,
"constraints": {"min": 1, "max": 999}
}
]
}Something broke on the server. Never expose stack traces or internal details.
{
"type": "https://api.example.com/errors/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred. Please try again or contact support.",
"error_code": "INTERNAL_ERROR",
"request_id": "req_abc123xyz"
}The request_id lets the user contact support with a reference. You look up the full stack trace in your logs using this ID.
Always return ALL validation errors at once, not just the first one. Nobody wants to play whack-a-mole:
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "3 validation errors in request body.",
"errors": [
{
"field": "email",
"message": "Must be a valid email address",
"code": "INVALID_FORMAT"
},
{
"field": "shipping_address.zip_code",
"message": "Must be a 5-digit US zip code",
"code": "INVALID_FORMAT"
},
{
"field": "items[0].quantity",
"message": "Must be a positive integer",
"code": "INVALID_TYPE"
}
]
}Use dot notation for nested fields and bracket notation for array indices:
| Field Path | Meaning |
|---|---|
email | Top-level field |
shipping_address.city | Nested object field |
items[0].quantity | First item's quantity |
items[2].product_id | Third item's product ID |
metadata.tags[1] | Second tag in metadata |
| Code | Meaning | Example |
|---|---|---|
REQUIRED | Field is missing | email is required |
INVALID_TYPE | Wrong data type | Expected integer, got string |
INVALID_FORMAT | Wrong format | Not a valid email/URL/date |
TOO_SHORT | Below minimum length | Must be at least 8 characters |
TOO_LONG | Above maximum length | Must be at most 255 characters |
OUT_OF_RANGE | Numeric value out of bounds | Must be between 1 and 999 |
NOT_UNIQUE | Duplicate value | Email already registered |
INVALID_ENUM | Not an allowed value | Must be one of: pending, shipped, delivered |
PATTERN_MISMATCH | Doesn't match regex | Must match pattern: ^[A-Z]{2}[0-9]{6}$ |
Include these headers on EVERY response (not just 429 errors):
X-RateLimit-Limit: 1000 # Max requests per window
X-RateLimit-Remaining: 742 # Requests left in current window
X-RateLimit-Reset: 1718460000 # Unix timestamp when window resets{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded 1000 requests per hour. Try again in 258 seconds.",
"error_code": "RATE_LIMIT_EXCEEDED",
"retry_after": 258,
"rate_limit": {
"limit": 1000,
"window": "1 hour",
"reset_at": "2026-06-15T15:00:00Z"
}
}Always include Retry-After header:
Retry-After: 258| Strategy | Description | Best For |
|---|---|---|
| Fixed window | N requests per hour/minute | Simple, predictable |
| Sliding window | N requests in the last 60 seconds | Smoother, prevents burst at window boundaries |
| Token bucket | Tokens refill at a steady rate | Allows short bursts while limiting sustained rate |
| Leaky bucket | Requests processed at fixed rate, excess queued | Smoothest output rate |
| Status Code | Retryable? | Why |
|---|---|---|
| 400 | No | Client needs to fix the request |
| 401 | No | Client needs new credentials |
| 403 | No | Permission issue -- retrying won't help |
| 404 | No | Resource doesn't exist |
| 409 | Maybe | If it's a race condition, retry might work |
| 422 | No | Business rule violation -- retrying won't help |
| 429 | Yes | Rate limit -- wait and retry |
| 500 | Yes | Transient server error -- retry might work |
| 502 | Yes | Upstream failure -- retry might work |
| 503 | Yes | Temporary overload -- retry after backoff |
For retryable errors, include a retry_after field:
{
"type": "https://api.example.com/errors/service-unavailable",
"title": "Service Temporarily Unavailable",
"status": 503,
"detail": "The order processing service is temporarily overloaded.",
"error_code": "SERVICE_UNAVAILABLE",
"retryable": true,
"retry_after": 30,
"retry_strategy": "exponential_backoff"
}Recommend this pattern in your API documentation:
import time
import random
def retry_with_backoff(request_func, max_retries=3, base_delay=1.0):
"""Retry a request with exponential backoff and jitter."""
for attempt in range(max_retries + 1):
response = request_func()
if response.status_code < 500 and response.status_code != 429:
return response # Non-retryable -- return immediately
if attempt == max_retries:
return response # Final attempt -- return whatever we got
# Calculate delay with exponential backoff + jitter
delay = base_delay * (2 ** attempt)
jitter = random.uniform(0, delay * 0.1)
# Respect Retry-After header if present
retry_after = response.headers.get("Retry-After")
if retry_after:
delay = max(delay, float(retry_after))
time.sleep(delay + jitter)
return responseGenerate a unique request ID for every API call:
# Response header
X-Request-Id: req_f47ac10b58cc4372
# Include in error responses
{
"request_id": "req_f47ac10b58cc4372"
}Accept a client-provided trace ID and echo it back:
# Request
X-Correlation-Id: trace_from_client_abc
# Response (echo the client's ID + add your own)
X-Correlation-Id: trace_from_client_abc
X-Request-Id: req_f47ac10b58cc4372For every error, log:
Do NOT log:
// BAD: exposes stack trace, database info, file paths
{
"error": "NullPointerException at OrderService.java:142",
"sql": "SELECT * FROM orders WHERE id = 'ord_abc'; -- injected",
"stack": ["at com.acmecorp.api.OrderService.getOrder(OrderService.java:142)", "..."]
}
// GOOD: generic message with request ID for internal lookup
{
"type": "https://api.example.com/errors/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred.",
"request_id": "req_f47ac10b"
}If a user requests a resource they don't have permission to see, should you return 403 (forbidden) or 404 (not found)?
| Response | Implication |
|---|---|
| 403 | Confirms the resource exists (information disclosure) |
| 404 | Hides the resource's existence |
Recommendation: Use 404 unless the user knowing the resource exists is not a security concern. Most APIs use 404 for resources the user can't access.
Don't return different error shapes for different auth failures. This helps attackers enumerate valid credentials:
// BAD: different messages reveal which part is wrong
{"error": "User not found"} // Reveals that the username is wrong
{"error": "Invalid password"} // Reveals that the username is correct
// GOOD: same message for all auth failures
{"error": "Invalid credentials"} // Doesn't reveal which part is wrongMaintain a central error catalog that maps error codes to HTTP status, titles, and detail templates:
ERROR_CATALOG = {
"VALIDATION_ERROR": {
"status": 422,
"title": "Validation Error",
"type_slug": "validation-error",
},
"NOT_FOUND": {
"status": 404,
"title": "Resource Not Found",
"type_slug": "not-found",
},
"RATE_LIMIT_EXCEEDED": {
"status": 429,
"title": "Rate Limit Exceeded",
"type_slug": "rate-limit-exceeded",
},
# ... more error codes
}1. Consistency: All endpoints return the same error format
2. Documentation: Auto-generate error reference from the catalog
3. Testing: Verify that error responses match the catalog
4. Localization: Map error codes to translated messages
See the src/error_envelope.py module for a complete Python implementation of this pattern.
*Previous: Pagination & Filtering | Next: Idempotency & Caching*
A reference for building reliable, high-performance APIs using idempotency keys, conditional requests (ETags), cache control headers, and stale-while-revalidate patterns.
3. Conditional Requests with ETags
6. Cache Invalidation Patterns
An operation is idempotent if performing it multiple times produces the same result as performing it once.
| Method | Idempotent? | Why |
|---|---|---|
| GET | Yes | Reading data doesn't change it |
| PUT | Yes | Replacing a resource with the same data = same result |
| DELETE | Yes | Deleting an already-deleted resource = still deleted |
| PATCH | No* | Relative updates like {"increment": 1} are not idempotent |
| POST | No | Creating a resource twice creates two resources |
*PATCH can be made idempotent if you use absolute values ({"status": "shipped"}) instead of relative ones ({"increment_total": 100}).
Network failures happen. Clients retry. Without idempotency:
Client → POST /orders → Server processes → Server responds
↑
Network drops response
Client → POST /orders → Server processes again → DUPLICATE ORDER CREATED
With idempotency:
Client → POST /orders (Idempotency-Key: abc) → Server processes → Server responds
↑
Network drops response
Client → POST /orders (Idempotency-Key: abc) → Server recognizes key → Returns cached response
NO DUPLICATE
1. The client generates a unique key (UUID v4) and sends it with the request
2. The server processes the request and stores the result keyed by the idempotency key
3. If the server receives the same key again, it returns the stored result instead of processing again
POST /orders
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"customer_id": "cust_42",
"items": [{"product_id": "prod_101", "quantity": 2}]
}Receive request with Idempotency-Key
├── Key not seen before
│ ├── Process the request
│ ├── Store: key → {status, headers, body}
│ └── Return the response
├── Key seen before, processing complete
│ └── Return the stored response (exact same status + body)
├── Key seen before, still processing
│ └── Return 409 Conflict ("Request is being processed")
└── Key seen before, different request body
└── Return 422 Unprocessable Entity ("Idempotency key reused with different parameters")
HTTP/1.1 201 Created
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Idempotent-Replayed: false # false on first call, true on replays
# On replay:
HTTP/1.1 201 Created
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Idempotent-Replayed: true # Signals this is a cached replay| Aspect | Recommendation |
|---|---|
| Storage backend | Redis or database table |
| Key format | UUID v4 (client-generated) |
| TTL | 24-72 hours (long enough for retries, short enough to not fill storage) |
| What to store | Status code, response headers, response body |
| Scope | Per API key / per user (don't share idempotency keys across users) |
import uuid
from datetime import datetime, timedelta
# Server-side idempotency store (simplified)
IDEMPOTENCY_STORE: dict[str, dict] = {}
KEY_TTL = timedelta(hours=24)
def handle_idempotent_request(
idempotency_key: str,
request_hash: str,
process_func,
) -> tuple[int, dict]:
"""Handle a request with idempotency.
Args:
idempotency_key: Client-provided UUID.
request_hash: Hash of the request body (to detect reuse with different params).
process_func: Callable that processes the request and returns (status, body).
Returns:
Tuple of (status_code, response_body).
"""
if idempotency_key in IDEMPOTENCY_STORE:
stored = IDEMPOTENCY_STORE[idempotency_key]
# Check if the key was used with different request parameters
if stored["request_hash"] != request_hash:
return 422, {
"error": "Idempotency key reused with different parameters"
}
# Check if the stored result has expired
if datetime.utcnow() - stored["created_at"] > KEY_TTL:
del IDEMPOTENCY_STORE[idempotency_key]
# Fall through to process the request
else:
# Return the cached result
stored["replayed"] = True
return stored["status"], stored["body"]
# Process the request
status, body = process_func()
# Store the result
IDEMPOTENCY_STORE[idempotency_key] = {
"status": status,
"body": body,
"request_hash": request_hash,
"created_at": datetime.utcnow(),
"replayed": False,
}
return status, body| Endpoint Type | Needs Idempotency? | Why |
|---|---|---|
| POST (create) | Yes | Prevents duplicate creation |
| POST (action, e.g., /cancel) | Yes | Prevents double-cancellation side effects |
| PUT (replace) | No | Already idempotent by definition |
| PATCH (update) | Maybe | If it has side effects (emails, webhooks) |
| DELETE | No | Already idempotent |
| GET | No | Already safe and idempotent |
An Entity Tag is a fingerprint of a resource's current state. It's a hash of the response body (or a version number) that the server includes in the response.
# Server response with ETag
HTTP/1.1 200 OK
ETag: "a3f2b8c1d4e56789"
Content-Type: application/json
{"id": "ord_abc123", "status": "pending", "total": 5999}| Type | Format | Meaning | Use For |
|---|---|---|---|
| Strong | "a3f2b8c1" | Byte-for-byte identical | Conditional updates, cache validation |
| Weak | W/"a3f2b8c1" | Semantically equivalent | Caching where minor formatting changes don't matter |
The client caches the response and the ETag. On the next request, it sends If-None-Match:
# Client sends cached ETag
GET /orders/ord_abc123
If-None-Match: "a3f2b8c1d4e56789"
# If nothing changed: 304 Not Modified (no body -- saves bandwidth)
HTTP/1.1 304 Not Modified
ETag: "a3f2b8c1d4e56789"
# If something changed: 200 OK with new data and new ETag
HTTP/1.1 200 OK
ETag: "new_etag_value"
Content-Type: application/json
{"id": "ord_abc123", "status": "shipped", "total": 5999}Prevent lost updates when two clients modify the same resource simultaneously:
# Client A reads the order (gets ETag)
GET /orders/ord_abc123
→ ETag: "version_1"
# Client A updates the order (sends If-Match with the ETag)
PUT /orders/ord_abc123
If-Match: "version_1"
Content-Type: application/json
{"status": "shipped"}
# Scenario 1: No one else modified it → 200 OK
HTTP/1.1 200 OK
ETag: "version_2"
# Scenario 2: Client B modified it first → 412 Precondition Failed
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/precondition-failed",
"title": "Precondition Failed",
"status": 412,
"detail": "The resource has been modified since you last retrieved it. Fetch the latest version and retry.",
"current_etag": "version_2_by_client_b"
}import hashlib
import json
def generate_etag(data: dict | str) -> str:
"""Generate a strong ETag from data.
Uses SHA-256 hash of the JSON-serialized data.
"""
if isinstance(data, dict):
content = json.dumps(data, sort_keys=True, separators=(",", ":"))
else:
content = str(data)
hash_value = hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
return f'"{hash_value}"'See the src/etag.py module for a complete implementation.
The Cache-Control header tells clients and intermediaries how to cache the response.
| Directive | Meaning | Example |
|---|---|---|
public | Any cache can store this | CDN-cacheable content |
private | Only the end-user's browser can cache this | User-specific data |
no-cache | Must revalidate with server before using cached copy | Always fresh, but allows caching |
no-store | Don't cache at all | Sensitive data, PII |
max-age=N | Cache for N seconds | max-age=3600 = 1 hour |
s-maxage=N | Cache for N seconds (shared caches / CDN only) | CDN-specific TTL |
must-revalidate | When stale, must revalidate before use | Critical data |
stale-while-revalidate=N | Serve stale content for N seconds while revalidating in background | Performance optimization |
immutable | Content will never change | Versioned assets |
| Endpoint Type | Cache-Control | Rationale |
|---|---|---|
| User profile | private, max-age=60 | User-specific, changes occasionally |
| Product catalog | public, max-age=300, s-maxage=600 | Shared data, CDN-friendly |
| Order details | private, no-cache | User-specific, needs freshness |
| Static reference data | public, max-age=86400, immutable | Country codes, currencies -- rarely change |
| Search results | private, max-age=30 | User-specific, changes often |
| Authenticated endpoint | private, no-store | Never cache auth-dependent data in shared caches |
| Health check | no-store | Must always be fresh |
# Product listing (cacheable by CDN for 10 minutes)
HTTP/1.1 200 OK
Cache-Control: public, max-age=300, s-maxage=600
ETag: "product_list_v42"
Vary: Accept, Accept-Encoding
# User's order history (only browser cache, revalidate always)
HTTP/1.1 200 OK
Cache-Control: private, no-cache
ETag: "user_orders_v15"
# Auth token response (never cache)
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cacheVary HeaderVary tells caches that different versions of the response exist based on request headers:
# This response varies by Accept header and Authorization
Vary: Accept, Accept-Encoding, AuthorizationWithout Vary, a CDN might serve a JSON response to a client requesting XML, or serve user A's data to user B.
stale-while-revalidate lets clients use a stale cached response immediately while fetching a fresh copy in the background:
Cache-Control: public, max-age=60, stale-while-revalidate=300This means:
0s 60s 360s
|--- fresh ------|--- stale-but-usable ---|--- expired ---|
↑
Background revalidation starts here
Client gets instant (stale) response
| Scenario | SWR Value | Rationale |
|---|---|---|
| Product catalog | stale-while-revalidate=600 | Slightly stale prices are OK for 10 min |
| User dashboard | stale-while-revalidate=30 | Short staleness OK, feels instant |
| Stock prices | Don't use SWR | Staleness is never acceptable |
| Blog posts | stale-while-revalidate=3600 | Posts rarely change, staleness is fine |
When a resource changes, invalidate its cache:
def update_order(order_id: str, updates: dict) -> dict:
"""Update an order and invalidate its cache."""
order = database.update(order_id, updates)
# Invalidate the specific order cache
cache.delete(f"order:{order_id}")
# Invalidate the order list cache (since the list content changed)
cache.delete_pattern("order_list:*")
return orderUse Surrogate-Key headers to group cached responses for bulk invalidation:
# Response for GET /orders/ord_abc123
Surrogate-Key: order-ord_abc123 customer-cust_42 orders
# When order changes, invalidate by surrogate key:
# PURGE all responses tagged with "order-ord_abc123"The safest caching strategy: always revalidate, but avoid re-downloading if nothing changed.
# Server response
Cache-Control: no-cache
ETag: "v42"
# Client's next request
GET /orders/ord_abc123
If-None-Match: "v42"
# Server response (nothing changed)
HTTP/1.1 304 Not Modified
# No body transferred -- saves bandwidthThis gives you freshness guarantees with bandwidth savings. The trade-off is a round-trip to the server on every request (but no data transfer if nothing changed).
# Cacheable by CDN (different TTL for CDN vs. browser)
Cache-Control: public, max-age=60, s-maxage=3600
Surrogate-Control: max-age=3600 # Fastly/Varnish-specific
Vary: Accept, Accept-Encoding # Important: tells CDN to cache variantsConfigure your CDN to cache different variants based on:
| Header | Segment By | Example |
|---|---|---|
Accept | Response format | JSON vs. XML |
Accept-Language | Language | English vs. Finnish |
Authorization | User | Never cache across users |
Accept-Encoding | Compression | gzip vs. brotli |
Your CDN cache key should include:
Vary header valuesExclude from cache key:
utm_source, ref)_t=123456)debug=true)| Pattern | Header | Use Case |
|---|---|---|
| Prevent duplicate creation | Idempotency-Key: | POST requests that create resources |
| Cache validation | ETag + If-None-Match | Bandwidth-efficient freshness checks |
| Optimistic locking | ETag + If-Match | Prevent lost updates |
| Browser caching | Cache-Control: private, max-age=60 | User-specific data |
| CDN caching | Cache-Control: public, s-maxage=3600 | Shared, public data |
| Instant but fresh | stale-while-revalidate=300 | Performance-critical endpoints |
| Never cache | Cache-Control: no-store | Sensitive / PII data |
*Previous: Error Handling | Back to README*
Get the full REST API Design Guide and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.