← Home
.NETWeb APIReported by candidates

How do you implement global exception handling and structured logging in a production ASP.NET Core API?

8 reports in the question index: Infosys (3), TCS (2), Deloitte, Accenture and Corporate Infotech, in first technical rounds, September and October 2026.

Say this first · about 30 seconds
Make it yours · Navistar Financial credit submissions (invented example)
The code, simplified

Invented to show the shape, in C#. Tap Next: the FIS core rejects application 77 from dealer 2231.

Navistar.sln · where these 2 files sit
  • Services/
SubmissionService.cs · line 2 · 1 of 7 and let the method wait for the FIS core without blocking a thread; is the "done later" wrapper ().

Fix later in your repo Example written for this card: invented, not from a real codebase.

Say it out loud, then check

Show the 30-second answer
  • I handle exceptions in one place, near the start of the middleware pipeline: UseExceptionHandler with an IExceptionHandler class, plus AddProblemDetails. Anything thrown further in, by a controller, a service or a filter, ends up there, as long as the response hasn't started yet.
  • The handler logs the full exception with its stack trace, and the client gets a 500 problem details body with a safe title, the trace id, which ASP.NET Core makes for the request, and our correlation id, which the portal sends. Never the exception message, because messages can carry SQL, server names or customer data.
  • Expected failures aren't logged as errors: a validation failure is a 400 and a missing record is a 404. I log with message templates, never string interpolation, and a scope puts the correlation id on every line.
Check your answer: tick what you said

You said 0 of 8

Then add your example: Navistar Financial credit submissions (invented example)
  • Situation: our API sends each truck loan application to the FIS core for a credit decision. Some come back rejected for bad data, like an invalid (the vehicle's 17-character serial number). We logged each one as plain text, "Submission failed for dealer 2231: _INVALID", so nobody could count rejections per dealer.
  • What I did: changed the log call to a message template with named fields, {DealerId} and {Reason}. In each one becomes its own column, so one query groups rejections by dealer and reason. I pinned that query to the team's dashboard.
  • Result: on the first day the dashboard showed one dealer behind 41 of 60 rejections: their own system added a space to every . called them with the numbers, and rejections dropped by two thirds.
  • What I'd say: structured logging turns logs into data you can query. The template stays the same for every call, and the values are separate fields.

See it

1/8A UDC customer opens a statement. The portal sends its correlation id, c-81f2, in a header.

In plain words

The senior answer · about 2 minutes
One place for unhandled errors
  • Every API I've worked on has one global handler: near the start of the pipeline, , and since .NET 8 an class. It catches whatever a controller, service or filter throws, logs it once with the full stack trace, and answers a 500 ProblemDetails. Try/catch in actions is only for errors the action can actually recover from.
What the client sees, and what it doesn't
  • The client gets a short title, the trace id (ASP.NET Core's own id for the request) and our correlation id (the one the portal sends). It never gets the exception message: at UDC the message, with the SQL server's name in it, went into every 500 for a week, the worst during a database failover ( switching to a standby), which is the incident in Deep. Support asks for the id on the screen and finds every log line of that request in one query.
Expected errors aren't 500s
  • Bad input is a 400 from [ApiController], a missing record is a 404 return value. Known business exceptions, like while the FIS core runs its nightly batch, get a small handler registered before the global one: it answers 409 with "try again after 3 am" and logs a warning, so it doesn't page anyone at night.
Logs you can query
  • I log with message templates, "Loading statement for {ContractId}", never $"...", so is a field in and I can find every request for one contract. A correlation-id middleware opens a logging scope before the exception handler, so even the error line carries the id. Levels are set per category in appsettings, Information for our code and Warning for Microsoft's in production, and an alert fires on the rate of 500s, not on single lines.

One failing request, in the logs

What holds a minute after the error, cut down to fit a phone. union traces, exceptions reads both tables at once: log lines land in traces, logged exceptions in exceptions. Levels are numbers: Information 1, Warning 2, Error 3.

1/5Many requests at once. Five lines from four requests, mixed together, as they really arrive.

The story

  1. Before

    UDC Finance's customer API has about thirty actions (controller methods that each answer one kind of request). In the first version, each one wrapped its work in its own try/catch (a block that tries the work and catches an exception, the error object thrown when something fails): it wrote the exception to the log and returned a 500 with some JSON. Every action did it slightly differently.

    1/4Before: every action catches its own exceptions and answers in its own shape.

  2. Why it had to change

    Copied error handling drifts. One action returned the raw exception message to customers; another forgot the catch, so the customer got an empty 500 with nothing to quote to support. The logs were plain sentences, so support couldn't find the lines for one customer's failed request among thousands.

  3. How it works now

    ASP.NET Core gives one place for this. A middleware is a step every request passes through, in order; the whole list of steps is the pipeline. The middleware , near the start of the pipeline, catches any exception thrown after it and hands it to your exception handler class. That class logs the full error and answers with a safe ProblemDetails body (the standard JSON error shape). For the logs, structured logging keeps every value as a searchable field, and a correlation id on every line ties one request's lines together. The main diagram follows one failing request through both.

  4. What we gained

    Actions only do their own work, every error answers the same safe way, and support goes from the id on the customer's screen to the exact log lines in one search. The rest of the card shows the handler chain, what never to log, and the week UDC's errors showed customers a server name.

In the lending API

The global handler: the one place an unhandled exception becomes a log line and a safe 500.

Only the key lines are shown; Deep walks the whole file.

Lending.sln · where this file sits
  • src/Udc.Customer.Api/Errors/
⋯ 1 line in Deep
GlobalExceptionHandler.cs · line 1 · 1 of 4The catch-all handler. A primary constructor: DI passes in the ProblemDetails writer (problems) and the logger (log). More: .

The code, file by file

The UDC customer API: start-up, the correlation id, a controller and the global handler. Tap Next to walk one failing request.

Lending.sln · where these 4 files sit
  • src/Udc.Customer.Api/
    • Middleware/
    • Controllers/
    • Errors/
Diagram · step 7The portal gets a 500 ProblemDetails: a safe title and c-81f2. No exception message, no server name.
Program.cs · line 4 · 1 of 7Errors leave as . The lines below add our correlation id to every one. (Set-up sits first in the file, so the walk reaches it last.)

What they ask next

The follow-ups interviews really ask. Answer out loud, then tap to check.

Follow-up tree

Answer, then open the next level. That is the order an interviewer digs in.

Reporteda candidate was asked Intervieweran interviewer asks it

Production bugs, and how to tell them

Each one: what you see, why, the fix in code, and the line to say.

Production incident: Error responses showed customers the database server's name

P2 · serious, has a workaroundUDC Financecame with a deployCustomer portal API (ASP.NET Core on App Service)

Composed, not a real event at UDC Finance. The mechanism is real ASP.NET Core, checked by a run on .NET 10: a handler that sets ProblemDetails.Detail to exception.Message sends that message to the client in every environment, and a handler that sets only a title sends no exception text. Times, counts, names, ticket and PR numbers are composed.

The story in 5 lines
  1. On Tue 13 Oct UDC's yearly penetration test (an outside firm attacking the system on purpose, with permission) reported that the portal's 500 errors showed the SQL server's name and the app's database login.
  2. Release 3.8 on Tue 6 Oct had replaced copied try/catch blocks with one global exception handler. The handler put exception.Message into the response's detail field.
  3. In that week 1,240 responses were 500s; 310 of them, during a database failover ( switching to a standby server) on Fri 9 Oct, carried "Login failed for user 'udc_portal_app'" and the server name. No customer data or passwords were in them.
  4. An the same afternoon removed the line. The fix added a test that fails if any exception text reaches a response, and the database login moved to a , so there is no SQL password left to guess.
  5. The rule since: error responses carry only text we wrote and the ids; the review checklist asks what reaches the client on an error.
Who's who (5 people)
You
.NET developer on the vendor team (L3). Owns the fix.
Tech lead
Vendor senior. Runs the bridge call, reviews every PR.
L2 pod
Vendor support. Runs queries, deploys with approval.
UDC security analyst
Owns the yearly penetration test and its findings.
UDC IT change manager
Approves the emergency change.

Tap a step. The map lights the place where it happens.

1/8 Detect

The pen test report: 500 responses show the SQL server's name and login.

Tue 13 Oct, 10:00 NZDT, New Zealand (02:30 IST)UDC security analyst, then the L2 pod

  • What: the yearly penetration test report has a new finding: "information disclosure". When the testers forced errors, the portal's 500 responses contained the SQL server's name and the database login the app uses.
  • How we saw it: the testers attached a screenshot of the portal's error box showing "Login failed for user 'udc_portal_app'". The security analyst raises it with the vendor team.
  • Why it hid: customers rarely see 500s, and when they did, the box looked like a normal error. Nobody had looked at what the new handler sent.
In theory

Error responses show only text the team wrote, plus an id.

In practice

For a week every 500 carried the raw exception message. An outside test found it, not our own checks.

Try it yourself · 10 minutes, on your Mac
  1. dotnet new web -n ErrLab. Add , an and app.UseExceptionHandler().
  2. Add app.MapGet("/boom", () => { throw new Exception("server sql-prod-01, login app_user"); });.
  3. In the handler set Detail = exception.Message, run it with ASPNETCORE_ENVIRONMENT=Production and call /boom: the server name is in the body.
  4. Remove that line and call it again: only the title and traceId. The message is still in the console log.
You should see

Whatever the handler puts in the body goes to the client, in every environment. Messages belong in the logs; the client gets text you wrote and an id.

The senior move

"An error response carries only text we wrote and an id; the details live in the logs, and a test proves it."

Sources Sourced: ASP.NET Core docs, "Handle errors in ASP.NET Core" (, , showing exception details only in Development); OWASP's guidance that error messages must not reveal internal details. Checked with a real run on .NET 10: Detail = exception.Message put the message in a Production 500 body; with only a title, the body held no exception text. Composed: UDC's systems, the pen test, dates, counts, names, ticket and numbers and the .

Traps

Traps

What should you say instead? Tap to check.

Live coding

Easy first, then harder. Set a timer and type it before you look.

Task 1

Reportedeasy · 15 minutes

Write a global exception handler (IExceptionHandler or middleware) that logs the exception and returns a 500 ProblemDetails with a correlation ID.

Infosys: how would you create a global exception handler?
Stuck? Hints
  1. Which interface does .NET 8 give you for this, and how do you register it?
  2. Where in Program.cs must the middleware go?
  3. Where does the correlation id come from, and how does it reach the response body?
Show a solution
Lending.sln · where these 2 files sit
Diagram · step 7The portal gets a 500 ProblemDetails: a safe title and c-81f2. No exception message, no server name.
Program.cs · line 1 · 1 of 3Adds the correlation id to every ProblemDetails.
What the interviewer checks
  • registered with AddExceptionHandler<T>(), plus and app.UseExceptionHandler() early.
  • LogError(exception, template, ...): the exception object is passed, with a fixed template.
  • 500 ProblemDetails with a safe title; no exception message.
  • Correlation id from a header or a new one, in a logging scope, in the body () and the response header.
  • Correlation middleware before , and can say why.

Task 2

Question index: Mustmedium · 15 minutes

BMW FS contracts are locked while the FIS core runs its nightly batch, and the service throws . Return 409 with a "try again later" ProblemDetails, logged as a warning, without touching the global handler.

Infosys: how do you handle production errors?
Stuck? Hints
  1. Can you register more than one ? In which order are they asked?
  2. What should a handler return for an exception that isn't its type?
  3. Which log level fits an expected, handled situation?
Show a solution
Lending.sln · where these 2 files sit
  • Errors/
Diagram · step 2The exception handler middleware asks handler 1, the first one registered.
Program.cs · line 2 · 1 of 2Registered first, so it's asked first.
What the interviewer checks
  • A second that returns false for any other exception.
  • Registered before , because the first handler that returns true wins.
  • 409 ProblemDetails with a title and a detail written by us; with the contract id as a field.
  • Can say why this isn't a 500: nothing is broken, so nobody should be paged.
With AI tools

Interviewers now ask how AI tools change this. Assistants write a global handler in seconds, and the same two problems keep showing up in what they generate: Detail = exception.Message "to help debugging", and log calls built with $"...". I check every generated handler for what reaches the client and every log call for a fixed template. AI also helps on the other side: I paste a correlation id's log lines into an assistant to summarise a long trace, after removing customer data.

Glossary: every name, one line each →

My notes

Paste the NOTES block from your doubt chat here. The grader never reads notes; the coach reads "Still unclear".

Read moreWhere it was asked