# Error Handling

https://dev.wisecp.com/es/error-handling

One handler catches everything and decides what the caller sees. That is why you throw instead of building error responses, and why a silent failure is almost always a caught one.

## Overview

Startup installs handlers for PHP errors, uncaught exceptions and fatal shutdowns. From that point nothing fails on its own terms. The handler records it, then shows it in the shape the request expects, an HTML page or a JSON body.

Inside an operation there is a second layer. The dispatcher wraps your method in a try block. An exception you throw becomes the error response, with your message as its text. That is the whole error path for a mutation: throw, and stop writing.

## Reference

### Raising a Failure

- **Operations**: Throw an exception whose message comes from the translation layer. The dispatcher turns it into the error response.
- **Modules**: Throw, exactly as an operation does. The caller catches it and surfaces the message. An older module that assigns to an `$error` property and returns false is carrying a pattern from the previous major version. The base classes still translate that into a throw, but new code does not write it.
- **Helpers**: Return a falsy value and let the caller decide, or throw when the caller is always an operation.

### Recording

```php
public static function getInstance(): MioException;
public function initialize(): void;

public function logError(string $message, array $context = [], string $level = Logger::LEVEL_ERROR, string $type = Logger::TYPE_SYSTEM): string;
public function logDatabaseError(string $query, string $error, array $params = []): string;
public function sanitizeData(array $data, int $depth = 0): array;   // $depth is internal recursion; pass nothing

public static function showErrors(): void;
```

```php
public static function getInstance(): Logger;
public function log(string $level, string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;

// One static per level, all with the same shape.
public static function debug(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function info(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function warning(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function error(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function fatal(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;

public static function database(string $query, string $error, array $params = []): string;

// Runs the callback, records anything it throws and returns null instead of rethrowing.
public static function safe(callable $callback, array $context = [], string $errorMessage = 'Operation failed');
```

- **MioException::getInstance()**: The handler. A singleton, installed during startup by `initialize()`.
- **logError()**: Records a failure with context and returns the identifier, which is the same identifier the caller is shown.
- **logDatabaseError()**: The same for a query. The statement comes first and the message second, and the context it builds is `['query' => ..., 'parameters' => ...]`.
- **sanitizeData()**: Returns a copy with sensitive values replaced. Run request data through it before logging anything.
- **Logger::error()**: The everyday form, one static per level. Same result as the handler's method, without reaching for the singleton.
- **Logger::safe()**: For work that must not take the request down with it, such as a provider call made while building a page.
- **Logger::findById()**: The record behind an Error ID, or `null`. That identifier is what a support request arrives with. It takes you from the customer's message to the stored context.

### Levels and Types

Both are plain strings, and the constants are the only spelling that is safe to write. Every recording call returns the identifier as a string. It is uppercase hexadecimal padded to at least six characters, followed by three digits, so `4F59C9BC644` rather than a fixed width. Store it as text and do not validate its length.

| Constant | Value | Use it for |
| --- | --- | --- |
| `Logger::LEVEL_DEBUG` | `DEBUG` | Development tracing, not for a shipped path. |
| `Logger::LEVEL_INFO` | `INFO` | Something happened that someone may want to confirm later. |
| `Logger::LEVEL_WARNING` | `WARNING` | Recovered, degraded, retried. |
| `Logger::LEVEL_ERROR` | `ERROR` | The default. The operation did not do what it was asked. |
| `Logger::LEVEL_FATAL` | `FATAL` | The request cannot continue. Written by the shutdown handler. |
| `Logger::LEVEL_DEPRECATED` | `DEPRECATED` | Engine deprecation notices, kept apart so they can be filtered out. |
| `Logger::TYPE_SYSTEM` | `system` | The default type, everything that is not a query. |
| `Logger::TYPE_DATABASE` | `database` | Query failures. Kept in a separate log so one noisy query cannot bury the rest. |

### What Redaction Actually Matches

A key is redacted when its lowercased name **contains** any of `password`, `pass`, `passwd`, `pwd`, `secret`, `token`, `key`, `auth`, `api_key` or `private_key`. It is a substring test, applied recursively to nested arrays, and it looks at keys only.

```php
$in = [
    'email'      => 'someone@example.test',
    'Password'   => 's3cr3t',            // matched case insensitively
    'api_token'  => 'abc123',            // contains "token"
    'keyword'    => 'hosting',           // contains "key" - redacted too
    'server'     => ['auth_user' => 'root', 'port' => 22],
];

$out = MioException::getInstance()->sanitizeData($in);

// [
//     'email'     => 'someone@example.test',
//     'Password'  => '***REDACTED***',
//     'api_token' => '***REDACTED***',
//     'keyword'   => '***REDACTED***',
//     'server'    => ['auth_user' => '***REDACTED***', 'port' => 22],
// ]
```

### What the Caller Sees

| Situation | Response |
| --- | --- |
| Exception thrown in an operation | `{"status":"error","message":"<your message>"}` |
| Error caught by the handler during an AJAX request | `{"status":"error","message":"Error ID: <id>: <text>","error_id":"<id>"}`, plus a `debug` member when diagnostics are on |
| Fatal during a page | The error page; detail only when diagnostics are on |
| Non-fatal warning | Recorded; visible on the page only when diagnostics are on |
| Anything on the command line | Recorded; the message can be swallowed by the output buffer |

## Example

```php
// In an operation: throw, and let the dispatcher answer.
public function delete_widget(Operation $operation): bool
{
    $operation->demo();

    $id = (int) Filter::init("POST/id", "rnumbers");
    if (!$id) throw new Exception(Language::gc("widgets/error-id-required"));

    if (!$this->model->delete($id)) throw new Exception(Language::gc("widgets/error-delete-failed"));

    return $operation->output(['status' => "successful"]);
}

// In a module: throw. The caller catches it and surfaces the message.
public function create(): array|false
{
    $response = $this->api->create($params);

    if (!($response['success'] ?? false))
        throw new Exception((string) ($response['message'] ?? $this->lang["err-provider"]));

    return ['status' => 'SUCCESS'];
}
```

```php
// Recording something you handled yourself. Redact first; the identifier that
// comes back is the one to quote in the message the operator will read.
$mio = MioException::getInstance();
$id  = $mio->logError("Provider refused the transfer", $mio->sanitizeData($_POST), Logger::LEVEL_WARNING);

throw new Exception(Language::gc("services/error-transfer", ['{id}' => $id]));

// The browser then receives, from the operation dispatcher:
//   {"status":"error","message":"Transfer failed. Reference: 4F59C9BC644"}
//
// Reading it back on the command line:
//   php coremio/errlog.php list --limit=10
//   php coremio/errlog.php show <signature>
```

## Pitfalls

> **Three neighbouring calls, three different first arguments**
> 
> `logError()` starts with the message, `logDatabaseError()` starts with the *query* and takes the message second, and `Logger::log()` starts with the level. Swapping the first two in the database call is silent. You get a log entry whose message is a SQL statement, and whose recorded query is an error text.

> **Every warning is a disk write**
> 
> The handler records non-fatal warnings too, and each record is a write. A missing array key read inside a loop costs one write per iteration. That is why values are read with a default rather than hopefully.

> **Redact before you log, and read the match rule first**
> 
> The log is a file that outlives the request and gets copied around. Redaction is a substring test on key names, so it is generous in one direction and blind in the other. A field named `card` or `answer` passes through untouched. Do not pass raw request data and assume the list covers your field.

> **A quiet failure is still recorded**
> 
> Nothing on screen means the handler decided not to show it, not that nothing happened. Read the log before concluding that a code path was never reached.

## Related Articles

- [Debugging and Logs](https://dev.wisecp.com/en/debugging-and-logs)
- [Operations](https://dev.wisecp.com/en/operations)
- [Coding Conventions](https://dev.wisecp.com/en/coding-conventions)
- [Request and Response Format](https://dev.wisecp.com/en/request-and-response-format)
