Skip to content

Commit 525ce08

Browse files
authored
Design a exception hierarchy for io_file (#250)
1 parent 9b1fffb commit 525ce08

File tree

2 files changed

+298
-0
lines changed

2 files changed

+298
-0
lines changed

pkgs/io_file/lib/src/exceptions.dart

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'file_system.dart';
6+
7+
// Design Notes:
8+
//
9+
// The [IOFileException] hierarchy is roughly based on the Java
10+
// `FileSystemException` hierarchy and the Python `OSError` hierarchy.
11+
//
12+
// There is no exception corresponding to the POSIX `EISDIR` error code because
13+
// there is no corresponding Windows error code.
14+
15+
/// An error related to call to the operating system or an intermediary library,
16+
/// such as libc.
17+
class SystemCallError {
18+
/// The name of the system call, such as `open` or `CreateFile`.
19+
final String systemCall;
20+
21+
/// The operating-system defined code for the error, such as 2 for
22+
/// `ERROR_FILE_NOT_FOUND` on Windows.
23+
final int errorCode;
24+
25+
/// The operating-system description of the error, such as
26+
/// "The system cannot find the file specified."
27+
final String message;
28+
29+
const SystemCallError(this.systemCall, this.errorCode, this.message);
30+
}
31+
32+
/// Exception thrown when a file operation fails.
33+
class IOFileException implements Exception {
34+
// A description of the failed operation.
35+
final String message;
36+
37+
/// A path provided in a failed file operation.
38+
///
39+
/// For file operations that involve a single path
40+
/// (e.g. [FileSystem.readAsBytes]), `path1` will be the provided path.
41+
///
42+
/// For file operations that involve two paths (e.g. [FileSystem.rename]),
43+
/// `path1` will be the first path argument.
44+
///
45+
/// Will be `null` for file operations that do not specify a path.
46+
final String? path1;
47+
48+
/// A path provided in a failed file operation.
49+
///
50+
/// For file operations that involve a single path
51+
/// (e.g. [FileSystem.readAsBytes]), [path1] will be set and `path2` will be
52+
/// `null`.
53+
///
54+
/// For file operations that involve two paths (e.g. [FileSystem.rename]),
55+
/// `path2` will be the second path argument.
56+
///
57+
/// Will be `null` for file operations that do not specify a path.
58+
final String? path2;
59+
60+
/// The underlying system call that failed.
61+
///
62+
/// Can be `null` if the exception is not raised due to a failed system call.
63+
final SystemCallError? systemCall;
64+
65+
const IOFileException(
66+
this.message, {
67+
this.path1,
68+
this.path2,
69+
this.systemCall,
70+
});
71+
72+
String _toStringHelper(String className) {
73+
final sb = StringBuffer('$className: $message');
74+
if (path1 != null) {
75+
sb.write(', path1="$path1"');
76+
}
77+
if (path2 != null) {
78+
sb.write(', path2="$path2"');
79+
}
80+
if (systemCall case final call?) {
81+
sb.write(
82+
' (${call.systemCall}: ${call.message}, errorCode=${call.errorCode})',
83+
);
84+
}
85+
return sb.toString();
86+
}
87+
88+
@override
89+
String toString() => _toStringHelper('IOFileException');
90+
}
91+
92+
/// Exception thrown when a file operation that only works on empty directories
93+
/// (such as [FileSystem.removeDirectory]) is requested on directory that is not
94+
/// empty.
95+
///
96+
/// This exception corresponds to errors such as `ENOTEMPTY` on POSIX systems
97+
/// and `ERROR_DIR_NOT_EMPTY` on Windows.
98+
class DirectoryNotEmptyException extends IOFileException {
99+
const DirectoryNotEmptyException(
100+
super.message, {
101+
super.path1,
102+
super.path2,
103+
super.systemCall,
104+
});
105+
106+
@override
107+
String toString() => _toStringHelper('DirectoryNotEmptyException');
108+
}
109+
110+
/// Exception thrown when a file operation (such as
111+
/// [FileSystem.writeAsString]) is requested on there is not enough available
112+
/// disk.
113+
///
114+
/// This exception corresponds to errors such as `ENOSPC` on POSIX systems and
115+
/// `ERROR_DISK_FULL` on Windows.
116+
class DiskFullException extends IOFileException {
117+
const DiskFullException(
118+
super.message, {
119+
super.path1,
120+
super.path2,
121+
super.systemCall,
122+
});
123+
124+
@override
125+
String toString() => _toStringHelper('DiskFullException');
126+
}
127+
128+
/// Exception thrown when a file operation (such as
129+
/// `FileSystem.remove`) is requested on directory.
130+
///
131+
/// This exception corresponds to errors such as `EISDIR` on POSIX systems and
132+
/// `ERROR_DIRECTORY` on Windows.
133+
class IsADirectoryException extends IOFileException {
134+
const IsADirectoryException(
135+
super.message, {
136+
super.path1,
137+
super.path2,
138+
super.systemCall,
139+
});
140+
141+
@override
142+
String toString() => _toStringHelper('IsADirectoryException');
143+
}
144+
145+
/// Exception thrown when a directory operation (such as
146+
/// [FileSystem.removeDirectory]) is requested on a non-directory.
147+
///
148+
/// This exception corresponds to error codes such as `ENOTDIR` on POSIX systems
149+
/// and `ERROR_DIRECTORY` on Windows.
150+
class NotADirectoryException extends IOFileException {
151+
const NotADirectoryException(
152+
super.message, {
153+
super.path1,
154+
super.path2,
155+
super.systemCall,
156+
});
157+
158+
@override
159+
String toString() => _toStringHelper('NotADirectoryException');
160+
}
161+
162+
/// Exception thrown when a file operation fails because the necessary access
163+
/// rights are not available.
164+
///
165+
/// This exception corresponds to error codes such as `EACCES` on POSIX systems
166+
/// and `ERROR_ACCESS_DENIED` on Windows.
167+
class PathAccessException extends IOFileException {
168+
const PathAccessException(
169+
super.message, {
170+
super.path1,
171+
super.path2,
172+
super.systemCall,
173+
});
174+
175+
@override
176+
String toString() => _toStringHelper('PathAccessException');
177+
}
178+
179+
/// Exception thrown when a file operation fails because the target path already
180+
/// exists.
181+
///
182+
/// This exception corresponds to error codes such as `EEXIST` on POSIX systems
183+
/// and `ERROR_ALREADY_EXISTS` on Windows.
184+
class PathExistsException extends IOFileException {
185+
const PathExistsException(
186+
super.message, {
187+
super.path1,
188+
super.path2,
189+
super.systemCall,
190+
});
191+
192+
@override
193+
String toString() => _toStringHelper('PathExistsException');
194+
}
195+
196+
/// Exception thrown when a file operation fails because the referenced file
197+
/// system object or objects do not exist.
198+
///
199+
/// This exception corresponds to error codes such as `ENOENT` on POSIX systems
200+
/// and `ERROR_FILE_NOT_FOUND` on Windows.
201+
class PathNotFoundException extends IOFileException {
202+
const PathNotFoundException(
203+
super.message, {
204+
super.path1,
205+
super.path2,
206+
super.systemCall,
207+
});
208+
209+
@override
210+
String toString() => _toStringHelper('PathNotFoundException');
211+
}
212+
213+
/// Exception thrown when there are too many open files.
214+
///
215+
/// This exception corresponds to error codes such as `EMFILE` on POSIX systems
216+
/// and `ERROR_TOO_MANY_OPEN_FILES` on Windows.
217+
class TooManyOpenFilesException extends IOFileException {
218+
const TooManyOpenFilesException(
219+
super.message, {
220+
super.path1,
221+
super.path2,
222+
super.systemCall,
223+
});
224+
225+
@override
226+
String toString() => _toStringHelper('TooManyOpenFilesException');
227+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@TestOn('vm')
6+
library;
7+
8+
import 'package:io_file/src/exceptions.dart';
9+
import 'package:test/test.dart';
10+
11+
void main() {
12+
group('exceptions', () {
13+
test('basic', () {
14+
expect(
15+
const IOFileException('cannot open file').toString(),
16+
'IOFileException: cannot open file',
17+
);
18+
});
19+
20+
test('with path1', () {
21+
expect(
22+
const IOFileException('cannot open file', path1: '/foo/bar').toString(),
23+
'IOFileException: cannot open file, path1="/foo/bar"',
24+
);
25+
});
26+
27+
test('with path2', () {
28+
expect(
29+
const IOFileException('cannot open file', path2: '/foo/bar').toString(),
30+
'IOFileException: cannot open file, path2="/foo/bar"',
31+
);
32+
});
33+
34+
test('with path1 and path2', () {
35+
expect(
36+
const IOFileException(
37+
'cannot rename file',
38+
path1: '/foo/baz',
39+
path2: '/foo/bar',
40+
).toString(),
41+
'IOFileException: cannot rename file, '
42+
'path1="/foo/baz", path2="/foo/bar"',
43+
);
44+
});
45+
46+
test('system call', () {
47+
expect(
48+
const IOFileException(
49+
'cannot open file',
50+
systemCall: SystemCallError('open', 13, 'permission denied'),
51+
).toString(),
52+
'IOFileException: cannot open file '
53+
'(open: permission denied, errorCode=13)',
54+
);
55+
});
56+
57+
test('all arguments', () {
58+
expect(
59+
const IOFileException(
60+
'cannot rename file',
61+
path1: '/foo/baz',
62+
path2: '/foo/bar',
63+
systemCall: SystemCallError('rename', 13, 'permission denied'),
64+
).toString(),
65+
'IOFileException: cannot rename file, '
66+
'path1="/foo/baz", path2="/foo/bar" '
67+
'(rename: permission denied, errorCode=13)',
68+
);
69+
});
70+
});
71+
}

0 commit comments

Comments
 (0)