Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/vs/base/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ export class ExpectedError extends Error {
readonly isExpected = true;
}

const noTelemetryFlag = '__vscodeNoTelemetry';
type ErrorWithNoTelemetryFlag = Error & { [noTelemetryFlag]?: boolean };

/**
* Error that when thrown won't be logged in telemetry as an unhandled error.
*/
Expand All @@ -306,6 +309,7 @@ export class ErrorNoTelemetry extends Error {
constructor(msg?: string) {
super(msg);
this.name = 'CodeExpectedError';
(<ErrorWithNoTelemetryFlag>this)[noTelemetryFlag] = true;
}

public static fromError(err: Error): ErrorNoTelemetry {
Expand All @@ -320,10 +324,25 @@ export class ErrorNoTelemetry extends Error {
}

public static isErrorNoTelemetry(err: Error): err is ErrorNoTelemetry {
return err.name === 'CodeExpectedError';
return err.name === 'CodeExpectedError' || (<ErrorWithNoTelemetryFlag>err)?.[noTelemetryFlag] === true;
}
}

/**
* Marks an error so that {@link ErrorNoTelemetry.isErrorNoTelemetry} detects it
* and the error telemetry pipeline does not report it as an unhandled error.
* Useful when the original error class (e.g. `FileSystemProviderError`) needs
* to be preserved for callers but the error itself represents an expected
* condition that should not surface in error telemetry. Unlike using
* {@link ErrorNoTelemetry} directly, this preserves the original error's
* `name`, prototype chain, and any additional properties (such as the error
* code that some IPC channels carry through `Error#name`).
*/
export function markAsErrorNoTelemetry<T extends Error>(error: T): T {
(<ErrorWithNoTelemetryFlag>error)[noTelemetryFlag] = true;
return error;
Comment thread
bryanchen-d marked this conversation as resolved.
}

/**
* This error indicates a bug.
* Do not throw this for invalid user input.
Expand Down
19 changes: 18 additions & 1 deletion src/vs/base/test/common/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import assert from 'assert';
import { toErrorMessage } from '../../common/errorMessage.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js';
import { transformErrorForSerialization, transformErrorFromSerialization } from '../../common/errors.js';
import { ErrorNoTelemetry, markAsErrorNoTelemetry, transformErrorForSerialization, transformErrorFromSerialization } from '../../common/errors.js';
import { assertType } from '../../common/types.js';

suite('Errors', () => {
Expand Down Expand Up @@ -84,4 +84,21 @@ suite('Errors', () => {
assert.strictEqual(deserializedError.cause?.message, 'Cause error');
assert.strictEqual(deserializedError.cause?.stack, serializedCause.stack);
});

test('markAsErrorNoTelemetry preserves error type while making it no-telemetry', function () {
class MyError extends Error {
override readonly name = 'MyError';
readonly code = 'MY_CODE';
}

const error = new MyError('boom');
const marked = markAsErrorNoTelemetry(error);

assert.strictEqual(marked, error, 'returns the same error instance');
assert.ok(error instanceof MyError, 'preserves the original error class');
assert.strictEqual(error.name, 'MyError', 'preserves the original error name');
assert.strictEqual(error.code, 'MY_CODE', 'preserves additional properties');
assert.strictEqual(error.message, 'boom', 'preserves the message');
assert.ok(ErrorNoTelemetry.isErrorNoTelemetry(error), 'is detected as ErrorNoTelemetry');
});
});
18 changes: 16 additions & 2 deletions src/vs/platform/files/node/diskFileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILog
import { AbstractDiskFileSystemProvider } from '../common/diskFileSystemProvider.js';
import { UniversalWatcherClient } from './watcher/watcherClient.js';
import { NodeJSWatcherClient } from './watcher/nodejs/nodejsClient.js';
import { markAsErrorNoTelemetry } from '../../../base/common/errors.js';

export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements
IFileSystemProviderWithFileReadWriteCapability,
Expand Down Expand Up @@ -846,6 +847,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple

let resultError: Error | string = error;
let code: FileSystemProviderErrorCode;
let isExpected = false;
switch (error.code) {
case 'ENOENT':
code = FileSystemProviderErrorCode.FileNotFound;
Expand All @@ -865,13 +867,25 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
break;
case 'ERR_UNC_HOST_NOT_ALLOWED':
resultError = `${error.message}. Please update the 'security.allowedUNCHosts' setting if you want to allow this host.`;
Comment thread
bryanchen-d marked this conversation as resolved.
code = FileSystemProviderErrorCode.Unknown;
// Treat UNC host restriction as a permission denial: it is a
// security policy preventing access to the host, not an
// unknown failure. The error is also marked as expected so it
// is not reported as an unhandled error in telemetry — the
// host allowlist is user-configured and access denials are a
// normal outcome rather than a bug.
code = FileSystemProviderErrorCode.NoPermissions;
isExpected = true;
break;
default:
code = FileSystemProviderErrorCode.Unknown;
}

return createFileSystemProviderError(resultError, code);
const providerError = createFileSystemProviderError(resultError, code);
if (isExpected) {
markAsErrorNoTelemetry(providerError);
}
Comment on lines +883 to +886
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

markAsErrorNoTelemetry(providerError) overwrites FileSystemProviderError.name. For file-system errors, the name field is used to carry the FileSystemProviderErrorCode across IPC boundaries (because the IPC channel only serializes message/name/stack for Error instances), and toFileSystemProviderErrorCode() relies on that name format when the error is no longer an actual FileSystemProviderError. With name set to CodeExpectedError, callers in other processes will see this as Unknown (and lose the intended NoPermissions semantics). Consider avoiding name mutation for FileSystemProviderError and instead propagating an explicit no-telemetry marker (e.g. include a noTelemetry/code field in IPC error serialization, or update the no-telemetry mechanism to not repurpose name).

Copilot uses AI. Check for mistakes.

return providerError;
}

private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {
Expand Down