Error handling and user communication
Objectives
By the end of the lesson, you can tell the forms of an error meant for the machine, the developer, and the user apart, you can write a good error message, you know the standard error body (Problem Details), and you can decide when automatic retrying is dangerous.
Why does an error have several viewpoints?
In a web system an "error" can mean several things: a failure of the connection between the browser and the server (4.6), faulty user input, a server-side exception, or a temporary outage of an external service (map, payment, login). The same phenomenon looks different to the user ("the order did not go through") and to the developer (a timeout, a rejected payment, a database error, a network interruption).
Error handling is therefore two tasks. On the one hand, the system must detect that the normal flow was interrupted, and should if possible move into a safe state. On the other hand, it must communicate: with machines (status code, error body), with developers (log), and with people (message). The first is the technical, the second the human side; neither replaces the other: a detailed log entry does not help someone who cannot submit their enrolment, and a kind message does not replace actually handling the error.
flowchart LR
H[Error] --> S[HTTP response: status code and error body]
H --> N[Log and diagnosis]
H --> F[Explanation for the user]
F --> K[Safe next step]
The HTTP response: status code and machine-readable error body
Lesson 2.4 discussed the status codes in detail: 4xx signals a problem with the request, 5xx a fault of the server or the systems behind it, 401/403 the difference between authentication and authorization, 429 rate limiting, 502/503/504 gateway and availability errors. The code is meant primarily for software actors: on its basis the client can decide, for example, to retry or to ask for login.
The code is too little for the client to also get a cause. RFC 9457 therefore describes a standard error format, Problem Details: a JSON body with the media type application/problem+json can contain these members:
| Member |
Meaning |
type |
The identifier (URI) of the error type; if missing, about:blank, that is, it has no meaning beyond the status code. |
title |
A short, human summary of the error type; it does not change from occurrence to occurrence. |
status |
The HTTP status code for this occurrence; the same as the actual response code. |
detail |
A human explanation of this occurrence; the client must not interpret it by machine. |
instance |
The identifier (URI) of the given occurrence. |
According to the standard, custom extension members are also allowed, which unknown clients must ignore; and the error body must not reveal internal details (for example a call stack), because that is a security risk. A tried example (a full course in Kurzuspont):
$ curl -i localhost:8095/api/jelentkezes-hibas
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{"type":"https://kurzuspont.example/hibak/betelt",
"title":"A kurzus betelt",
"status":409,
"detail":"A Webprogramozás 1. kurzusra nincs szabad hely.",
"instance":"/api/jelentkezesek/kerelem/8841"}
(The values are Hungarian sample data: "A kurzus betelt" means "The course is full", and the detail "A Webprogramozás 1. kurzusra nincs szabad hely." means "There is no free place for the Web Programming 1 course.")
The client decides what to do on the basis of the type (for example it offers another course); the detail is meant for a human reader. This form uses the same error format for every endpoint, so clients do not have to guess (5.3).
What does a good error message look like?
A message addressed to the user answers four questions: what did not succeed; what consequence it has; what the user can do now; and whether their work was preserved. The tone should be calm and precise.
| Weak |
Better |
| "An error occurred." |
"We could not save your enrolment. The data you filled in so far has been kept. Check your internet connection, then try again. If the error does not go away, quote the error code: ABC-123." |
| "You entered bad data!" |
"A postal code consists of five digits." (next to the field) |
| "Error 500" |
"A temporary error occurred while saving. No enrolment was made, nothing was charged." |
The message should not blame the user, and specific feedback attached to the field can be fixed faster than a general warning at the top of the page (9.2). Security is also a limit: on a public page database names, file paths, internal IP addresses, or detailed stack traces must not appear (chapter 8): they help an attacker too. These details belong in the log, and the interface shows a summary that helps action.
The message must be true. If the response of the payment provider is uncertain, it is harmful to write that "the order surely failed", because the amount may have been charged after all. Better: "We are still checking the state of the payment. Do not start another payment; we will notify you by email shortly."
Recovery and retrying
Not every error gets the same response. If the user mistyped their email address, the system should show the possibility of correcting it. If a short network disturbance occurred, retrying can make sense. If the state of a financial transaction is uncertain, automatic or repeated submission can cause a duplicated operation. This is the idempotency concept of lesson 2.3 in practice.
One way to defend is the idempotency key: the client generates a unique key for every payment intention and sends the same one with every repetition; from the key the server recognizes that the request has already been executed, and returns the original result. The IETF HTTPAPI working group has designed a header named Idempotency-Key for this (currently an Internet-Draft, that is, not a finished standard), but in practice several services use a similar solution. On the trial server:
Without a key, sent twice:
{"tranzakcio":"T1","osszeg":5000}
{"tranzakcio":"T2","osszeg":5000} <- we charged twice
With the header Idempotency-Key: 5f1c-ab12, sent twice:
{"tranzakcio":"T3","osszeg":5000}
{"tranzakcio":"T3","osszeg":5000,"ismetelt":true} <- we charged once
(In the JSON, "tranzakcio" is the transaction, "osszeg" the amount, and "ismetelt" "repeated".)
For retrying, the server can give guidance with the Retry-After header (with 429 and 503): it can give seconds or a date until which the client must wait. A retrying client must follow this, and on repeated errors must increase the waiting, not re-request immediately and without limit (10.5).
A good system also preserves the user's work. With a long form it is especially frustrating if after a temporary error every filled-in field disappears. Periodic saving, local preservation (3.6), or a server-side draft greatly improves the experience: fault tolerance means not only the duplication of servers but also the protection of the user's work.
Status page: public state, not an advertising surface
A status page is a separate interface where the provider informs interested parties about the current state of important components, planned maintenance, and known outages. It is especially valuable when the main application is faulty. If the status page runs on the same faulty system, it will be unreachable exactly when it would be needed most, so it is worth running it on separate infrastructure.
A good status page shows, split by components, for example the state of login, the API, file upload, and payment; it describes in short, time-stamped updates where the fault stands (detected, investigating, cause identified, fix being applied, recovered, monitoring). Not every internal detail has to be made public, but a "we are working on it" unchanged for hours destroys trust. A status page does not replace direct information to those directly affected by a fault (an in-interface message, email, an institutional channel).
Worked example: a connection dropping during payment in Kurzuspont
Some courses in Kurzuspont are paid (for example a summer lab). The student clicks the "Pay" button, then the browser's connection drops. The screen does not know for sure whether the charge happened. A bad solution is to write immediately "Payment failed" and let the user pay again; it is just as bad to automatically resend the transaction.
| Step |
Responsible response |
| The client sends a unique idempotency key with the payment |
Repetition does not double the charge. |
| The server checks the state of the transaction by the key |
It can be decided whether it ran. |
| If uncertain |
It says with 202 or a similar signal: processing. |
| The message of the interface |
"We are still checking the state of the payment. Do not start another payment. We will send the result by email." |
| Log |
The event identifier, the key, and the time, without password and card data. |
| Status page |
If the payment provider is down, it is listed there as well. |
This protects the financial process and reduces the user's uncertainty at once.
Common misconceptions
| Statement |
Clarification |
| "A detailed technical error message is always useful." |
For the developer yes, for the user often not, and it is a security risk; the detail belongs in the log. |
| "Every 4xx error is the user's fault." |
A 4xx signals a problem with the request, but a bad interface or an outdated client can also cause a faulty request. |
| "A status page is only needed by big companies." |
For a critical service affecting many users it is a valuable trust tool. |
| "Retrying is harmless." |
With a payment or a booking it can cause a duplicated operation; an idempotency key is needed. |
| "An error message is just text." |
It is part of the system's business and human behavior. |
In brief
- An error has three audiences: the machine (status code, error body), the developer (log), the human (message).
- The
application/problem+json format of RFC 9457 gives a uniform, machine-readable error body.
- A good message says what did not succeed, what the consequence is, what the user can do, and whether their work was preserved; it reveals no internal details.
- For an operation in an uncertain state (payment), repetition is safe only with an idempotency key.
- A status page is public information, updated per component, running on separate infrastructure.
Review questions
- What does a Problem Details error body contain?
- Why should we not display the stack trace to the user?
- How does the idempotency key protect against a double charge?
- What must a good error message contain?
Learning resources
Error handling and user communication
Objectives
By the end of the lesson, you can tell the forms of an error meant for the machine, the developer, and the user apart, you can write a good error message, you know the standard error body (Problem Details), and you can decide when automatic retrying is dangerous.
Why does an error have several viewpoints?
In a web system an "error" can mean several things: a failure of the connection between the browser and the server (4.6), faulty user input, a server-side exception, or a temporary outage of an external service (map, payment, login). The same phenomenon looks different to the user ("the order did not go through") and to the developer (a timeout, a rejected payment, a database error, a network interruption).
Error handling is therefore two tasks. On the one hand, the system must detect that the normal flow was interrupted, and should if possible move into a safe state. On the other hand, it must communicate: with machines (status code, error body), with developers (log), and with people (message). The first is the technical, the second the human side; neither replaces the other: a detailed log entry does not help someone who cannot submit their enrolment, and a kind message does not replace actually handling the error.
flowchart LR H[Error] --> S[HTTP response: status code and error body] H --> N[Log and diagnosis] H --> F[Explanation for the user] F --> K[Safe next step]The HTTP response: status code and machine-readable error body
Lesson 2.4 discussed the status codes in detail: 4xx signals a problem with the request, 5xx a fault of the server or the systems behind it,
401/403the difference between authentication and authorization,429rate limiting,502/503/504gateway and availability errors. The code is meant primarily for software actors: on its basis the client can decide, for example, to retry or to ask for login.The code is too little for the client to also get a cause. RFC 9457 therefore describes a standard error format, Problem Details: a JSON body with the media type
application/problem+jsoncan contain these members:typeabout:blank, that is, it has no meaning beyond the status code.titlestatusdetailinstanceAccording to the standard, custom extension members are also allowed, which unknown clients must ignore; and the error body must not reveal internal details (for example a call stack), because that is a security risk. A tried example (a full course in Kurzuspont):
(The values are Hungarian sample data: "A kurzus betelt" means "The course is full", and the detail "A Webprogramozás 1. kurzusra nincs szabad hely." means "There is no free place for the Web Programming 1 course.")
The client decides what to do on the basis of the
type(for example it offers another course); thedetailis meant for a human reader. This form uses the same error format for every endpoint, so clients do not have to guess (5.3).What does a good error message look like?
A message addressed to the user answers four questions: what did not succeed; what consequence it has; what the user can do now; and whether their work was preserved. The tone should be calm and precise.
The message should not blame the user, and specific feedback attached to the field can be fixed faster than a general warning at the top of the page (9.2). Security is also a limit: on a public page database names, file paths, internal IP addresses, or detailed stack traces must not appear (chapter 8): they help an attacker too. These details belong in the log, and the interface shows a summary that helps action.
The message must be true. If the response of the payment provider is uncertain, it is harmful to write that "the order surely failed", because the amount may have been charged after all. Better: "We are still checking the state of the payment. Do not start another payment; we will notify you by email shortly."
Recovery and retrying
Not every error gets the same response. If the user mistyped their email address, the system should show the possibility of correcting it. If a short network disturbance occurred, retrying can make sense. If the state of a financial transaction is uncertain, automatic or repeated submission can cause a duplicated operation. This is the idempotency concept of lesson 2.3 in practice.
One way to defend is the idempotency key: the client generates a unique key for every payment intention and sends the same one with every repetition; from the key the server recognizes that the request has already been executed, and returns the original result. The IETF HTTPAPI working group has designed a header named
Idempotency-Keyfor this (currently an Internet-Draft, that is, not a finished standard), but in practice several services use a similar solution. On the trial server:(In the JSON, "tranzakcio" is the transaction, "osszeg" the amount, and "ismetelt" "repeated".)
For retrying, the server can give guidance with the
Retry-Afterheader (with429and503): it can give seconds or a date until which the client must wait. A retrying client must follow this, and on repeated errors must increase the waiting, not re-request immediately and without limit (10.5).A good system also preserves the user's work. With a long form it is especially frustrating if after a temporary error every filled-in field disappears. Periodic saving, local preservation (3.6), or a server-side draft greatly improves the experience: fault tolerance means not only the duplication of servers but also the protection of the user's work.
Status page: public state, not an advertising surface
A status page is a separate interface where the provider informs interested parties about the current state of important components, planned maintenance, and known outages. It is especially valuable when the main application is faulty. If the status page runs on the same faulty system, it will be unreachable exactly when it would be needed most, so it is worth running it on separate infrastructure.
A good status page shows, split by components, for example the state of login, the API, file upload, and payment; it describes in short, time-stamped updates where the fault stands (detected, investigating, cause identified, fix being applied, recovered, monitoring). Not every internal detail has to be made public, but a "we are working on it" unchanged for hours destroys trust. A status page does not replace direct information to those directly affected by a fault (an in-interface message, email, an institutional channel).
Worked example: a connection dropping during payment in Kurzuspont
Some courses in Kurzuspont are paid (for example a summer lab). The student clicks the "Pay" button, then the browser's connection drops. The screen does not know for sure whether the charge happened. A bad solution is to write immediately "Payment failed" and let the user pay again; it is just as bad to automatically resend the transaction.
202or a similar signal: processing.This protects the financial process and reduces the user's uncertainty at once.
Common misconceptions
In brief
application/problem+jsonformat of RFC 9457 gives a uniform, machine-readable error body.Review questions
Learning resources